Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
Drarig29 committed Jan 9, 2025
1 parent 9237d83 commit 7b248c5
Show file tree
Hide file tree
Showing 4 changed files with 343 additions and 11 deletions.
124 changes: 114 additions & 10 deletions .github/workflows/e2e/tests.synthetics.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,123 @@
{
"tests": [
{
"id": "https://app.datadoghq.com/synthetics/details/pwd-mwg-3p5",
"config": {
"headers": {
"X-Fake-Header": "fake value"
}
"local_test_definition": {
"public_id": "77k-qct-9he",
"name": "[cg] Test on shopist",
"type": "browser",
"config": {
"assertions": [],
"configVariables": [],
"request": {
"method": "GET",
"headers": {},
"url": "https://shopist.io"
},
"setCookie": "",
"variables": []
},
"options": {
"device_ids": ["chrome.laptop_large"],
"ignoreServerCertificateError": false,
"disableCors": false,
"disableCsp": false,
"noScreenshot": false,
"retry": {
"count": 3,
"interval": 230
},
"rumSettings": {
"isEnabled": false
}
},
"locations": ["aws:eu-central-1"],
"steps": [
{
"name": "Click on div \"Shop now\"",
"params": {
"element": {
"url": "https://shopist.io/",
"bucketKey": "e2e-tests/1737979/step-elements/2025-01-09T09:53:47.833774_steps.json",
"targetOuterHTML": "<div data-v-fc3720cd=\"\">Shop now</div>",
"userLocator": {
"values": [
{
"type": "xpath",
"value": "/*[local-name()=\"html\"][1]/*[local-name()=\"body\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"section\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"section\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"a\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][3]"
}
],
"failTestOnCannotLocate": true
}
}
},
"type": "click",
"allowFailure": false,
"isCritical": true,
"noScreenshot": false,
"exitIfSucceed": false,
"alwaysExecute": false
}
]
}
},
{
"id": "https://app.datadoghq.com/synthetics/details/2r9-q7u-4nn",
"config": {
"headers": {
"X-Fake-Header": "fake value"
}
"local_test_definition": {
"public_id": "eb9-5g9-k5u",
"name": "[cg] Test on shopist",
"type": "browser",
"config": {
"assertions": [],
"configVariables": [],
"request": {
"method": "GET",
"headers": {},
"url": "https://shopist.io"
},
"setCookie": "",
"variables": []
},
"options": {
"device_ids": ["chrome.laptop_large"],
"ignoreServerCertificateError": false,
"disableCors": false,
"disableCsp": false,
"noScreenshot": false,
"retry": {
"count": 3,
"interval": 230
},
"rumSettings": {
"isEnabled": false
}
},
"locations": ["aws:eu-central-1"],
"steps": [
{
"name": "Click on div \"Shop now\"",
"params": {
"element": {
"url": "https://shopist.io/",
"bucketKey": "e2e-tests/1737979/step-elements/2025-01-09T09:53:47.833774_steps.json",
"targetOuterHTML": "<div data-v-fc3720cd=\"\">Shop now</div>",
"userLocator": {
"values": [
{
"type": "xpath",
"value": "/*[local-name()=\"html\"][1]/*[local-name()=\"body\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"section\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"section\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"a\"][1]/*[local-name()=\"div\"][1]/*[local-name()=\"div\"][3]"
}
],
"failTestOnCannotLocate": true
}
}
},
"type": "click",
"allowFailure": false,
"isCritical": true,
"noScreenshot": false,
"exitIfSucceed": false,
"alwaysExecute": false
}
]
}
}
]
Expand Down
19 changes: 19 additions & 0 deletions src/commands/synthetics/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
Trigger,
MobileAppUploadResult,
MobileApplicationNewVersionParams,
LocalTestDefinition,
} from './interfaces'
import {MAX_TESTS_TO_TRIGGER} from './run-tests-command'
import {ciTriggerApp, getDatadogHost, retry} from './utils/public'
Expand Down Expand Up @@ -83,6 +84,23 @@ const triggerTests = (request: (args: AxiosRequestConfig) => AxiosPromise<Trigge
return resp.data
}

const editTest = (request: (args: AxiosRequestConfig) => AxiosPromise<Trigger>) => async (
testId: string,
data: LocalTestDefinition
) => {
const resp = await retryRequest(
{
data,
method: 'PUT',
url: `/synthetics/tests/${testId}`,
},
request,
{retryOn429: true}
)

return resp.data
}

const getTest = (request: (args: AxiosRequestConfig) => AxiosPromise<ServerTest>) => async (testId: string) => {
const resp = await retryRequest(
{
Expand Down Expand Up @@ -353,6 +371,7 @@ export const apiConstructor = (configuration: APIConfiguration) => {
getBatch: getBatch(request),
getMobileApplicationPresignedURLs: getMobileApplicationPresignedURLs(requestUnstable),
getTest: getTest(request),
editTest: editTest(request),
getSyntheticsOrgSettings: getSyntheticsOrgSettings(request),
getTunnelPresignedURL: getTunnelPresignedURL(requestIntake),
pollResults: pollResults(request),
Expand Down
3 changes: 2 additions & 1 deletion src/commands/synthetics/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {DeployTestsCommand} from './deploy-tests-command'
import {RunTestsCommand} from './run-tests-command'
import {UploadApplicationCommand} from './upload-application-command'

module.exports = [RunTestsCommand, UploadApplicationCommand]
module.exports = [RunTestsCommand, UploadApplicationCommand, DeployTestsCommand]
208 changes: 208 additions & 0 deletions src/commands/synthetics/deploy-tests-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import {Command, Option} from 'clipanion'
import deepExtend from 'deep-extend'
import terminalLink from 'terminal-link'

import {FIPS_ENV_VAR, FIPS_IGNORE_ERROR_ENV_VAR} from '../../constants'
import {getCommonAppBaseURL} from '../../helpers/app'
import {toBoolean} from '../../helpers/env'
import {enableFips} from '../../helpers/fips'
import {removeUndefinedValues, resolveConfigFromFile} from '../../helpers/utils'
import {isValidDatadogSite} from '../../helpers/validation'

import {EndpointError, formatBackendErrors, getApiHelper} from './api'
import {CiError} from './errors'
import {LocalTestDefinition, MainReporter, Reporter, RunTestsCommandConfig} from './interfaces'
import {DefaultReporter} from './reporters/default'
import {getTestConfigs} from './test'
import {isLocalTriggerConfig} from './utils/internal'
import {getReporter, reportCiError} from './utils/public'

export const DEFAULT_COMMAND_CONFIG = {
apiKey: '',
appKey: '',
configPath: 'datadog-ci.json',
datadogSite: 'datadoghq.com',
files: [],
proxy: {protocol: 'http'},
publicIds: [],
subdomain: 'app',
}

const configurationLink = 'https://docs.datadoghq.com/continuous_testing/cicd_integrations/configuration'

const $1 = (text: string) => terminalLink(text, `${configurationLink}#global-configuration-file-options`)
const $2 = (text: string) => terminalLink(text, `${configurationLink}#test-files`)

export class DeployTestsCommand extends Command {
public static paths = [['synthetics', 'deploy-tests']]

public static usage = Command.Usage({
category: 'Synthetics',
description: 'Deploy Local Test Definitions as scheduled tests in Datadog.',
details: `
This command deploys Local Test Definitions as scheduled tests in Datadog, usually when a feature branch is merged.
`,
examples: [
[
'Explicitly specify multiple tests to run',
'datadog-ci synthetics deploy-tests --public-id pub-lic-id1 --public-id pub-lic-id2',
],
[
'Override the default glob pattern',
'datadog-ci synthetics deploy-tests -f ./component-1/**/*.synthetics.json -f ./component-2/**/*.synthetics.json',
],
],
})

public configPath = Option.String('--config', {description: `Pass a path to a ${$1('global configuration file')}.`})

private apiKey = Option.String('--apiKey', {description: 'The API key used to query the Datadog API.'})
private appKey = Option.String('--appKey', {description: 'The application key used to query the Datadog API.'})
private datadogSite = Option.String('--datadogSite', {description: 'The Datadog instance to which request is sent.'})
private files = Option.Array('-f,--files', {
description: `Glob pattern to detect Synthetic test ${$2('configuration files')}}.`,
})
private publicIds = Option.Array('-p,--public-id', {description: 'Specify a test to run.'})
private subdomain = Option.String('--subdomain', {
description:
'The name of the custom subdomain set to access your Datadog application. If the URL used to access Datadog is `myorg.datadoghq.com`, the `subdomain` value needs to be set to `myorg`.',
})

private reporter!: MainReporter
private config: RunTestsCommandConfig = JSON.parse(JSON.stringify(DEFAULT_COMMAND_CONFIG)) // Deep copy to avoid mutation

private fips = Option.Boolean('--fips', false)
private fipsIgnoreError = Option.Boolean('--fips-ignore-error', false)
private fipsConfig = {
fips: toBoolean(process.env[FIPS_ENV_VAR]) ?? false,
fipsIgnoreError: toBoolean(process.env[FIPS_IGNORE_ERROR_ENV_VAR]) ?? false,
}

public async execute() {
enableFips(this.fips || this.fipsConfig.fips, this.fipsIgnoreError || this.fipsConfig.fipsIgnoreError)

const reporters: Reporter[] = [new DefaultReporter(this)]
this.reporter = getReporter(reporters)

try {
await this.resolveConfig()
} catch (error) {
if (error instanceof CiError) {
reportCiError(error, this.reporter)
}

return 1
}

const testConfigs = await getTestConfigs(this.config, this.reporter)

const first = isLocalTriggerConfig(testConfigs[0]) ? testConfigs[0] : undefined
if (!first) {
throw new Error('No test configurations found')
}

const api = getApiHelper(this.config)

const publicId = first.local_test_definition.public_id! // or passed publicId without id-less LTD

const existingRemoteTest = await api.getTest(publicId)

const remoteTest = {
...existingRemoteTest,
...first.local_test_definition,
config: {
...existingRemoteTest.config,
...first.local_test_definition.config,
},
options: {
...existingRemoteTest.options,
...first.local_test_definition.options,
},
} as LocalTestDefinition

/* eslint-disable @typescript-eslint/no-unsafe-member-access */
delete (remoteTest as any).creator
delete (remoteTest as any).monitor_id
delete (remoteTest as any).created_at
delete (remoteTest as any).modified_at
delete (remoteTest as any).public_id
/* eslint-enable @typescript-eslint/no-unsafe-member-access */

// TODO: If public ID is passed, they are used for selection among the JSON files. If one public ID is not found, crash. If no public ID is passed, all JSON files are used.

try {
await api.editTest(publicId, remoteTest)

const baseUrl = getCommonAppBaseURL(this.config.datadogSite, this.config.subdomain)
const testLink = `${baseUrl}synthetics/details/${publicId}`

// If we had a version property in the response, we could have a link of the form: https://dd.datad0g.com/synthetics/version/77k-qct-9he?versionUUID=<new-version-uuid>
// Or we could use the `https://dd.datad0g.com/api/v2/synthetics/tests/77k-qct-9he/version_history` endpoint to get the version history info, and show some info about the diff - or at least know the latest version.

this.reporter.log(`Test ${publicId} has been successfully edited: ${testLink}\n`)
} catch (e) {
const errorMessage = formatBackendErrors(e)
throw new EndpointError(`[${publicId}] Failed to edit test: ${errorMessage}\n`, e.response?.status)
}
}

private async resolveConfig() {
// Defaults < file < ENV < CLI

// Override with config file variables (e.g. datadog-ci.json)
try {
// Override Config Path with ENV variables
const overrideConfigPath = this.configPath ?? process.env.DATADOG_SYNTHETICS_CONFIG_PATH ?? 'datadog-ci.json'
this.config = await resolveConfigFromFile(this.config, {
configPath: overrideConfigPath,
defaultConfigPaths: [this.config.configPath],
})
} catch (error) {
if (this.configPath) {
throw error
}
}

// Override with ENV variables
this.config = deepExtend(
this.config,
removeUndefinedValues({
apiKey: process.env.DATADOG_API_KEY,
appKey: process.env.DATADOG_APP_KEY,
configPath: process.env.DATADOG_SYNTHETICS_CONFIG_PATH, // Only used for debugging
datadogSite: process.env.DATADOG_SITE,
files: process.env.DATADOG_SYNTHETICS_FILES?.split(';'),
publicIds: process.env.DATADOG_SYNTHETICS_PUBLIC_IDS?.split(';'),
subdomain: process.env.DATADOG_SUBDOMAIN,
})
)

// Override with CLI parameters
this.config = deepExtend(
this.config,
removeUndefinedValues({
apiKey: this.apiKey,
appKey: this.appKey,
configPath: this.configPath,
datadogSite: this.datadogSite,
files: this.files,
publicIds: this.publicIds,
subdomain: this.subdomain,
})
)

if (typeof this.config.files === 'string') {
this.reporter.log('[DEPRECATED] "files" should be an array of string instead of a string.\n')
this.config.files = [this.config.files]
}

if (!isValidDatadogSite(this.config.datadogSite)) {
throw new CiError(
'INVALID_CONFIG',
`The \`datadogSite\` config property (${JSON.stringify(
this.config.datadogSite
)}) must match one of the sites supported by Datadog.\nFor more information, see "Site parameter" in our documentation: https://docs.datadoghq.com/getting_started/site/#access-the-datadog-site`
)
}
}
}

0 comments on commit 7b248c5

Please sign in to comment.