@datadog/datadog-ci 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -129,30 +129,36 @@ describe('run-test', () => {
129
129
  expect(startTunnelSpy).toHaveBeenCalledTimes(1);
130
130
  expect(stopTunnelSpy).toHaveBeenCalledTimes(1);
131
131
  }));
132
- test('getTestsList throws', () => __awaiter(void 0, void 0, void 0, function* () {
133
- const serverError = new Error('Server Error');
134
- serverError.response = { data: { errors: ['Bad Gateway'] }, status: 502 };
135
- serverError.config = { baseURL: 'baseURL', url: 'url' };
136
- const apiHelper = {
137
- searchTests: jest.fn(() => {
138
- throw serverError;
139
- }),
140
- };
141
- jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
142
- yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { testSearchQuery: 'a-search-query', tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG'));
143
- }));
144
- test('getTestsToTrigger throws', () => __awaiter(void 0, void 0, void 0, function* () {
145
- const serverError = new Error('Server Error');
146
- serverError.response = { data: { errors: ['Bad Gateway'] }, status: 502 };
147
- serverError.config = { baseURL: 'baseURL', url: 'url' };
148
- const apiHelper = {
149
- getTest: jest.fn(() => {
150
- throw serverError;
151
- }),
152
- };
153
- jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
154
- yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { publicIds: ['public-id-1'], tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG'));
155
- }));
132
+ const cases = [
133
+ [403, 'AUTHORIZATION_ERROR'],
134
+ [502, 'UNAVAILABLE_TEST_CONFIG'],
135
+ ];
136
+ describe.each(cases)('%s triggers %s', (status, error) => {
137
+ test(`getTestsList throws - ${status}`, () => __awaiter(void 0, void 0, void 0, function* () {
138
+ const serverError = new Error('Server Error');
139
+ serverError.response = { data: { errors: ['Error'] }, status };
140
+ serverError.config = { baseURL: 'baseURL', url: 'url' };
141
+ const apiHelper = {
142
+ searchTests: jest.fn(() => {
143
+ throw serverError;
144
+ }),
145
+ };
146
+ jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
147
+ yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { testSearchQuery: 'a-search-query', tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError(error));
148
+ }));
149
+ test(`getTestsToTrigger throws - ${status}`, () => __awaiter(void 0, void 0, void 0, function* () {
150
+ const serverError = new Error('Server Error');
151
+ serverError.response = { data: { errors: ['Bad Gateway'] }, status };
152
+ serverError.config = { baseURL: 'baseURL', url: 'url' };
153
+ const apiHelper = {
154
+ getTest: jest.fn(() => {
155
+ throw serverError;
156
+ }),
157
+ };
158
+ jest.spyOn(runTests, 'getApiHelper').mockImplementation(() => apiHelper);
159
+ yield expect(runTests.executeTests(fixtures_1.mockReporter, Object.assign(Object.assign({}, fixtures_1.ciConfig), { publicIds: ['public-id-1'], tunnel: true }))).rejects.toMatchError(new errors_1.CriticalError(error));
160
+ }));
161
+ });
156
162
  test('getPresignedURL throws', () => __awaiter(void 0, void 0, void 0, function* () {
157
163
  jest.spyOn(utils, 'getTestsToTrigger').mockReturnValue(Promise.resolve({
158
164
  overriddenTestsToTrigger: [],
@@ -87,27 +87,36 @@ describe('utils', () => {
87
87
  test('runTests sends batch metadata', () => __awaiter(void 0, void 0, void 0, function* () {
88
88
  jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => undefined);
89
89
  const payloadMetadataSpy = jest.fn();
90
- const axiosMock = jest.spyOn(axios_1.default, 'create');
91
- axiosMock.mockImplementation((() => (e) => {
92
- payloadMetadataSpy(e.data.metadata);
93
- if (e.url === '/synthetics/tests/trigger/ci') {
90
+ jest.spyOn(axios_1.default, 'create').mockImplementation((() => (request) => {
91
+ payloadMetadataSpy(request.data.metadata);
92
+ if (request.url === '/synthetics/tests/trigger/ci') {
94
93
  return { data: fakeTrigger };
95
94
  }
96
95
  }));
97
96
  yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
98
- expect(payloadMetadataSpy).toHaveBeenCalledWith({
99
- ci: { job: {}, pipeline: {}, provider: {}, stage: {} },
100
- git: { commit: { author: {}, committer: {} } },
101
- trigger_app: 'npm_package',
102
- });
97
+ expect(payloadMetadataSpy).toHaveBeenCalledWith(undefined);
103
98
  const metadata = {
104
99
  ci: { job: { name: 'job' }, pipeline: {}, provider: { name: 'jest' }, stage: {} },
105
100
  git: { commit: { author: {}, committer: {}, message: 'test' } },
106
101
  };
107
102
  jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => metadata);
103
+ yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
104
+ expect(payloadMetadataSpy).toHaveBeenCalledWith(metadata);
105
+ }));
106
+ test('runTests api call includes trigger app header', () => __awaiter(void 0, void 0, void 0, function* () {
107
+ jest.spyOn(ciHelpers, 'getCIMetadata').mockImplementation(() => undefined);
108
+ const headersMetadataSpy = jest.fn();
109
+ jest.spyOn(axios_1.default, 'create').mockImplementation((() => (request) => {
110
+ headersMetadataSpy(request.headers);
111
+ if (request.url === '/synthetics/tests/trigger/ci') {
112
+ return { data: fakeTrigger };
113
+ }
114
+ }));
115
+ yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
116
+ expect(headersMetadataSpy).toHaveBeenCalledWith(expect.objectContaining({ 'X-Trigger-App': 'npm_package' }));
108
117
  utils.setCiTriggerApp('unit_test');
109
118
  yield utils.runTests(api, [{ public_id: fakeId, executionRule: interfaces_1.ExecutionRule.NON_BLOCKING }]);
110
- expect(payloadMetadataSpy).toHaveBeenCalledWith(Object.assign(Object.assign({}, metadata), { trigger_app: 'unit_test' }));
119
+ expect(headersMetadataSpy).toHaveBeenCalledWith(expect.objectContaining({ 'X-Trigger-App': 'unit_test' }));
111
120
  }));
112
121
  test('should run test with publicId from url', () => __awaiter(void 0, void 0, void 0, function* () {
113
122
  const axiosMock = jest.spyOn(axios_1.default, 'create');
@@ -1,5 +1,5 @@
1
1
  import { AxiosError } from 'axios';
2
- import { APIConfiguration, Payload, PollResult, Test, TestSearchResult, Trigger } from './interfaces';
2
+ import { APIConfiguration, APIHelper } from './interfaces';
3
3
  interface BackendError {
4
4
  errors: string[];
5
5
  }
@@ -9,17 +9,8 @@ export declare class EndpointError extends Error {
9
9
  constructor(message: string, status: number);
10
10
  }
11
11
  export declare const formatBackendErrors: (requestError: AxiosError<BackendError>) => string;
12
+ export declare const isForbiddenError: (error: AxiosError | EndpointError) => boolean;
12
13
  export declare const isNotFoundError: (error: AxiosError | EndpointError) => boolean;
13
14
  export declare const is5xxError: (error: AxiosError | EndpointError) => boolean | 0 | undefined;
14
- export declare const apiConstructor: (configuration: APIConfiguration) => {
15
- getPresignedURL: (testIds: string[]) => Promise<{
16
- url: string;
17
- }>;
18
- getTest: (testId: string) => Promise<Test>;
19
- pollResults: (resultIds: string[]) => Promise<{
20
- results: PollResult[];
21
- }>;
22
- searchTests: (query: string) => Promise<TestSearchResult>;
23
- triggerTests: (data: Payload) => Promise<Trigger>;
24
- };
15
+ export declare const apiConstructor: (configuration: APIConfiguration) => APIHelper;
25
16
  export {};
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.apiConstructor = exports.is5xxError = exports.isNotFoundError = exports.formatBackendErrors = exports.EndpointError = void 0;
12
+ exports.apiConstructor = exports.is5xxError = exports.isNotFoundError = exports.isForbiddenError = exports.formatBackendErrors = exports.EndpointError = void 0;
13
13
  const querystring_1 = require("querystring");
14
14
  const utils_1 = require("../../helpers/utils");
15
15
  const utils_2 = require("./utils");
@@ -43,6 +43,7 @@ exports.formatBackendErrors = formatBackendErrors;
43
43
  const triggerTests = (request) => (data) => __awaiter(void 0, void 0, void 0, function* () {
44
44
  const resp = yield retryRequest({
45
45
  data,
46
+ headers: { 'X-Trigger-App': utils_2.ciTriggerApp },
46
47
  method: 'POST',
47
48
  url: '/synthetics/tests/trigger/ci',
48
49
  }, request);
@@ -88,6 +89,8 @@ const retryOn5xxErrors = (retries, error) => {
88
89
  }
89
90
  };
90
91
  const getErrorHttpStatus = (error) => { var _a; return 'status' in error ? error.status : (_a = error.response) === null || _a === void 0 ? void 0 : _a.status; };
92
+ const isForbiddenError = (error) => getErrorHttpStatus(error) === 403;
93
+ exports.isForbiddenError = isForbiddenError;
91
94
  const isNotFoundError = (error) => getErrorHttpStatus(error) === 404;
92
95
  exports.isNotFoundError = isNotFoundError;
93
96
  const is5xxError = (error) => {
@@ -142,6 +142,10 @@ class RunTestCommand extends clipanion_1.Command {
142
142
  reporter.log('No test to run.\n');
143
143
  break;
144
144
  // Critical command errors
145
+ case 'AUTHORIZATION_ERROR':
146
+ reporter.error(`\n${chalk_1.default.bgRed.bold(' ERROR: authorization error ')}\n${error.message}\n\n`);
147
+ reporter.log('Credentials refused, make sure `apiKey`, `appKey` and `datadogSite` are correct.\n');
148
+ break;
145
149
  case 'MISSING_APP_KEY':
146
150
  reporter.error(`Missing ${chalk_1.default.red.bold('DATADOG_APP_KEY')} in your environment.\n`);
147
151
  break;
@@ -1,8 +1,8 @@
1
1
  declare const nonCriticalErrorCodes: readonly ["NO_TESTS_TO_RUN", "NO_RESULTS_TO_POLL"];
2
- declare type NonCriticalCiErrorCode = typeof nonCriticalErrorCodes[number];
3
- declare const criticalErrorCodes: readonly ["UNAVAILABLE_TEST_CONFIG", "MISSING_API_KEY", "MISSING_APP_KEY", "UNAVAILABLE_TUNNEL_CONFIG", "TUNNEL_START_FAILED", "TRIGGER_TESTS_FAILED", "POLL_RESULTS_FAILED"];
4
- declare type CriticalCiErrorCode = typeof criticalErrorCodes[number];
5
- declare type CiErrorCode = NonCriticalCiErrorCode | CriticalCiErrorCode;
2
+ export declare type NonCriticalCiErrorCode = typeof nonCriticalErrorCodes[number];
3
+ declare const criticalErrorCodes: readonly ["AUTHORIZATION_ERROR", "MISSING_API_KEY", "MISSING_APP_KEY", "POLL_RESULTS_FAILED", "TRIGGER_TESTS_FAILED", "TUNNEL_START_FAILED", "UNAVAILABLE_TEST_CONFIG", "UNAVAILABLE_TUNNEL_CONFIG"];
4
+ export declare type CriticalCiErrorCode = typeof criticalErrorCodes[number];
5
+ export declare type CiErrorCode = NonCriticalCiErrorCode | CriticalCiErrorCode;
6
6
  export declare class CiError extends Error {
7
7
  code: CiErrorCode;
8
8
  constructor(code: CiErrorCode);
@@ -4,13 +4,14 @@ exports.CriticalError = exports.CiError = void 0;
4
4
  /* tslint:disable:max-classes-per-file */
5
5
  const nonCriticalErrorCodes = ['NO_TESTS_TO_RUN', 'NO_RESULTS_TO_POLL'];
6
6
  const criticalErrorCodes = [
7
- 'UNAVAILABLE_TEST_CONFIG',
7
+ 'AUTHORIZATION_ERROR',
8
8
  'MISSING_API_KEY',
9
9
  'MISSING_APP_KEY',
10
- 'UNAVAILABLE_TUNNEL_CONFIG',
11
- 'TUNNEL_START_FAILED',
12
- 'TRIGGER_TESTS_FAILED',
13
10
  'POLL_RESULTS_FAILED',
11
+ 'TRIGGER_TESTS_FAILED',
12
+ 'TUNNEL_START_FAILED',
13
+ 'UNAVAILABLE_TEST_CONFIG',
14
+ 'UNAVAILABLE_TUNNEL_CONFIG',
14
15
  ];
15
16
  class CiError extends Error {
16
17
  constructor(code) {
@@ -253,12 +253,9 @@ export interface ConfigOverride {
253
253
  };
254
254
  }
255
255
  export interface Payload {
256
- metadata?: SyntheticsMetadata;
256
+ metadata?: Metadata;
257
257
  tests: TestPayload[];
258
258
  }
259
- export declare type SyntheticsMetadata = Metadata & {
260
- trigger_app: string;
261
- };
262
259
  export interface TestPayload extends ConfigOverride {
263
260
  executionRule: ExecutionRule;
264
261
  public_id: string;
@@ -195,7 +195,7 @@ class DefaultReporter {
195
195
  this.write(error);
196
196
  }
197
197
  initErrors(errors) {
198
- this.write(errors.join('\n') + '\n');
198
+ this.write(errors.join('\n') + '\n\n');
199
199
  }
200
200
  log(log) {
201
201
  this.write(log);
@@ -246,7 +246,7 @@ class DefaultReporter {
246
246
  testsList.push('…');
247
247
  }
248
248
  const testsDisplay = chalk_1.default.gray(`(${testsList.join(', ')})`);
249
- this.write(`\nWaiting for ${chalk_1.default.bold.cyan(tests.length)} test result${tests.length > 1 ? 's' : ''} ${testsDisplay}…\n`);
249
+ this.write(`Waiting for ${chalk_1.default.bold.cyan(tests.length)} test result${tests.length > 1 ? 's' : ''} ${testsDisplay}…\n`);
250
250
  }
251
251
  testTrigger(test, testId, executionRule, config) {
252
252
  const idDisplay = `[${chalk_1.default.bold.dim(testId)}]`;
@@ -62,15 +62,5 @@ export declare const getTestsList: (api: APIHelper, config: SyntheticsCIConfig,
62
62
  id: string;
63
63
  suite: string | undefined;
64
64
  }[]>;
65
- export declare const getApiHelper: (config: SyntheticsCIConfig) => {
66
- getPresignedURL: (testIds: string[]) => Promise<{
67
- url: string;
68
- }>;
69
- getTest: (testId: string) => Promise<Test>;
70
- pollResults: (resultIds: string[]) => Promise<{
71
- results: PollResult[];
72
- }>;
73
- searchTests: (query: string) => Promise<import("./interfaces").TestSearchResult>;
74
- triggerTests: (data: import("./interfaces").Payload) => Promise<Trigger>;
75
- };
65
+ export declare const getApiHelper: (config: SyntheticsCIConfig) => APIHelper;
76
66
  export declare const getDatadogHost: (useIntake: boolean | undefined, config: SyntheticsCIConfig) => string;
@@ -32,7 +32,7 @@ const executeTests = (reporter, config) => __awaiter(void 0, void 0, void 0, fun
32
32
  testsToTrigger = yield exports.getTestsList(api, config, reporter);
33
33
  }
34
34
  catch (error) {
35
- throw new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG');
35
+ throw new errors_1.CriticalError(api_1.isForbiddenError(error) ? 'AUTHORIZATION_ERROR' : 'UNAVAILABLE_TEST_CONFIG');
36
36
  }
37
37
  }
38
38
  if (!testsToTrigger.length) {
@@ -43,7 +43,10 @@ const executeTests = (reporter, config) => __awaiter(void 0, void 0, void 0, fun
43
43
  testsToTriggerResult = yield utils_1.getTestsToTrigger(api, testsToTrigger, reporter);
44
44
  }
45
45
  catch (error) {
46
- throw error instanceof errors_1.CiError ? error : new errors_1.CriticalError('UNAVAILABLE_TEST_CONFIG');
46
+ if (error instanceof errors_1.CiError) {
47
+ throw error;
48
+ }
49
+ throw new errors_1.CriticalError(api_1.isForbiddenError(error) ? 'AUTHORIZATION_ERROR' : 'UNAVAILABLE_TEST_CONFIG');
47
50
  }
48
51
  const { tests, overriddenTestsToTrigger, summary } = testsToTriggerResult;
49
52
  // All tests have been skipped or are missing.
@@ -1,5 +1,6 @@
1
1
  import { APIHelper, ConfigOverride, ExecutionRule, InternalTest, MainReporter, PollResult, Reporter, Result, Suite, Summary, TestPayload, Trigger, TriggerConfig, TriggerResponse, TriggerResult } from './interfaces';
2
2
  import { Tunnel } from './tunnel';
3
+ export declare let ciTriggerApp: string;
3
4
  export declare const handleConfig: (test: InternalTest, publicId: string, reporter: MainReporter, config?: ConfigOverride | undefined) => TestPayload;
4
5
  export declare const setCiTriggerApp: (source: string) => void;
5
6
  export declare const getExecutionRule: (test: InternalTest, configOverride?: ConfigOverride | undefined) => ExecutionRule;
@@ -31,7 +31,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  return (mod && mod.__esModule) ? mod : { "default": mod };
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.parseVariablesFromCli = exports.retry = exports.runTests = exports.getTestsToTrigger = exports.getReporter = exports.getResultDuration = exports.createSummary = exports.createTriggerResultMap = exports.waitForResults = exports.wait = exports.getSuites = exports.hasTestSucceeded = exports.hasResultPassed = exports.isCriticalError = exports.getStrictestExecutionRule = exports.getExecutionRule = exports.setCiTriggerApp = exports.handleConfig = void 0;
34
+ exports.parseVariablesFromCli = exports.retry = exports.runTests = exports.getTestsToTrigger = exports.getReporter = exports.getResultDuration = exports.createSummary = exports.createTriggerResultMap = exports.waitForResults = exports.wait = exports.getSuites = exports.hasTestSucceeded = exports.hasResultPassed = exports.isCriticalError = exports.getStrictestExecutionRule = exports.getExecutionRule = exports.setCiTriggerApp = exports.handleConfig = exports.ciTriggerApp = void 0;
35
35
  const fs = __importStar(require("fs"));
36
36
  const path = __importStar(require("path"));
37
37
  const url_1 = require("url");
@@ -48,7 +48,7 @@ const PUBLIC_ID_REGEX = /^[\d\w]{3}-[\d\w]{3}-[\d\w]{3}$/;
48
48
  const SUBDOMAIN_REGEX = /(.*?)\.(?=[^\/]*\..{2,5})/;
49
49
  const TEMPLATE_REGEX = /{{\s*([^{}]*?)\s*}}/g;
50
50
  const template = (st, context) => st.replace(TEMPLATE_REGEX, (match, p1) => (p1 in context ? context[p1] : match));
51
- let ciTriggerApp = 'npm_package';
51
+ exports.ciTriggerApp = 'npm_package';
52
52
  const handleConfig = (test, publicId, reporter, config) => {
53
53
  const executionRule = exports.getExecutionRule(test, config);
54
54
  let handledConfig = {
@@ -86,7 +86,7 @@ const handleConfig = (test, publicId, reporter, config) => {
86
86
  };
87
87
  exports.handleConfig = handleConfig;
88
88
  const setCiTriggerApp = (source) => {
89
- ciTriggerApp = source;
89
+ exports.ciTriggerApp = source;
90
90
  };
91
91
  exports.setCiTriggerApp = setCiTriggerApp;
92
92
  const parseUrlVariables = (url, reporter) => {
@@ -425,8 +425,9 @@ exports.getTestsToTrigger = getTestsToTrigger;
425
425
  const runTests = (api, testsToTrigger) => __awaiter(void 0, void 0, void 0, function* () {
426
426
  const payload = { tests: testsToTrigger };
427
427
  const ciMetadata = ci_1.getCIMetadata();
428
- const syntheticsMetadata = Object.assign(Object.assign({ ci: { job: {}, pipeline: {}, provider: {}, stage: {} }, git: { commit: { author: {}, committer: {} } } }, ciMetadata), { trigger_app: ciTriggerApp });
429
- payload.metadata = syntheticsMetadata;
428
+ if (ciMetadata) {
429
+ payload.metadata = ciMetadata;
430
+ }
430
431
  try {
431
432
  return yield api.triggerTests(payload);
432
433
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datadog/datadog-ci",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Run datadog actions from the CI.",
5
5
  "repository": "https://github.com/DataDog/datadog-ci",
6
6
  "license": "Apache-2.0",