1 | 'use strict';
|
2 |
|
3 | const pako = require('pako');
|
4 | const _ = require('lodash');
|
5 | const testimCustomToken = require('./testimCustomToken');
|
6 | const constants = require('./constants');
|
7 | const Promise = require('bluebird');
|
8 | const utils = require('../utils.js');
|
9 | const config = require('./config');
|
10 | const httpRequest = require('./httpRequest');
|
11 |
|
12 | const runnerVersion = utils.getRunnerVersion();
|
13 | const logger = require('./logger').getLogger('testim service api');
|
14 | const hash = require('object-hash');
|
15 | const { ArgError } = require('../errors');
|
16 |
|
17 | const DEFAULT_REQUEST_RETRY = 3;
|
18 |
|
19 | function getTokenHeader() {
|
20 | return testimCustomToken.getCustomTokenV3()
|
21 | .then(brearToken => {
|
22 | if (!brearToken) {
|
23 | return Promise.reject(new Error('Failed to get token from server'));
|
24 | }
|
25 | return { Authorization: `Bearer ${brearToken}` };
|
26 | });
|
27 | }
|
28 |
|
29 | function postAuth({
|
30 | url, body, headers = {}, timeout, retry,
|
31 | }) {
|
32 | return getTokenHeader()
|
33 | .then(tokenHeaders => {
|
34 | const finalHeaders = Object.assign({}, headers, tokenHeaders);
|
35 | return httpRequest.post({
|
36 | url: `${config.SERVICES_HOST}${url || ''}`,
|
37 | body,
|
38 | headers: finalHeaders,
|
39 | timeout,
|
40 | retry,
|
41 | });
|
42 | });
|
43 | }
|
44 |
|
45 | function postAuthFormData(url, fields, files, headers = {}, timeout) {
|
46 | return getTokenHeader()
|
47 | .then(tokenHeaders => {
|
48 | const finalHeaders = Object.assign({}, headers, tokenHeaders);
|
49 | return httpRequest.postForm(`${config.SERVICES_HOST}${url || ''}`, fields, files, finalHeaders, timeout);
|
50 | });
|
51 | }
|
52 |
|
53 | function putAuth(url, body) {
|
54 | return getTokenHeader()
|
55 | .then(headers => httpRequest.put(`${config.SERVICES_HOST}${url || ''}`, body, headers));
|
56 | }
|
57 |
|
58 | function getTextWithAuth(url, query) {
|
59 | return getTokenHeader()
|
60 | .then(headers => httpRequest.getText(`${config.SERVICES_HOST}${url || ''}`, query, headers));
|
61 | }
|
62 |
|
63 | function getWithAuth(url, query, options) {
|
64 | return getTokenHeader()
|
65 | .then(headers => httpRequest.get(`${config.SERVICES_HOST}${url || ''}`, query, headers, undefined, options));
|
66 | }
|
67 |
|
68 | function getS3Artifact(url) {
|
69 | return utils.runWithRetries(() => getWithAuth(`/storage${url}`, null, { isBinary: true }));
|
70 | }
|
71 |
|
72 | function getTestPlan(projectId, testPlanNames) {
|
73 | return utils.runWithRetries(() => getWithAuth('/testPlan', { projectId, name: testPlanNames.join(',') }))
|
74 | .then(body => body.map(testPlan => {
|
75 | testPlan.testConfigIds = testPlan.testConfigIds ? JSON.parse(testPlan.testConfigIds) : [];
|
76 | testPlan.beforeAllLabels = testPlan.beforeAllLabels ? JSON.parse(testPlan.beforeAllLabels) : [];
|
77 | testPlan.testLabels = testPlan.testLabels ? JSON.parse(testPlan.testLabels) : [];
|
78 | testPlan.afterAllLabels = testPlan.afterAllLabels ? JSON.parse(testPlan.afterAllLabels) : [];
|
79 | return testPlan;
|
80 | }));
|
81 | }
|
82 |
|
83 | function saveTestPlanResult(projectId, testPlanId, result) {
|
84 | return utils.runWithRetries(() => postAuth({ url: '/testPlan/result', body: { projectId, testPlanId, result } }));
|
85 | }
|
86 |
|
87 | function updateTestStatus(status, executionId, testId, resultId, startTime, endTime, success, failureReason, config, projectId, remoteRunId) {
|
88 | return utils.runWithRetries(() => putAuth('/result/run/test', {
|
89 | runId: executionId,
|
90 | testId,
|
91 | startTime,
|
92 | endTime,
|
93 | success,
|
94 | failureReason,
|
95 | resultId,
|
96 | status,
|
97 | config,
|
98 | projectId,
|
99 | runnerVersion,
|
100 | remoteRunId,
|
101 | }), DEFAULT_REQUEST_RETRY);
|
102 | }
|
103 |
|
104 | function updateExecutionTests(executionId, runnerStatuses, status, reason, success, startTime, endTime, projectId) {
|
105 | return utils.runWithRetries(() => putAuth('/result/run/tests', {
|
106 | runId: executionId,
|
107 | runnerStatuses,
|
108 | status,
|
109 | reason,
|
110 | success,
|
111 | startTime,
|
112 | endTime,
|
113 | projectId,
|
114 | }), DEFAULT_REQUEST_RETRY);
|
115 | }
|
116 |
|
117 | function reportExecutionStarted(executionId, projectId, labels, startTime, executions, config, resultLabels, remoteRunId) {
|
118 | const isCiRun = require('../cli/isCiRun').isCi;
|
119 | return postAuth({
|
120 | url: '/result/run',
|
121 | body: {
|
122 | runId: executionId,
|
123 | projectId,
|
124 | labels,
|
125 | startTime,
|
126 | execution: executions,
|
127 | status: 'RUNNING',
|
128 | config,
|
129 | resultLabels,
|
130 | remoteRunId,
|
131 | metadata: {
|
132 | isCiRun,
|
133 | },
|
134 | },
|
135 | retry: 3,
|
136 | });
|
137 | }
|
138 |
|
139 | function reportExecutionFinished(status, executionId, projectId, success, suppressTmsReporting = false, tmsRunId = '', remoteRunId) {
|
140 | const endTime = Date.now();
|
141 |
|
142 | return utils.runWithRetries(() => putAuth('/result/run', {
|
143 | runId: executionId,
|
144 | projectId,
|
145 | endTime,
|
146 | status,
|
147 | success,
|
148 | suppressTmsReporting,
|
149 | tmsRunId,
|
150 | remoteRunId,
|
151 | }), DEFAULT_REQUEST_RETRY);
|
152 | }
|
153 |
|
154 | function getBranchData(projectId, branchName) {
|
155 | return utils.runWithRetries(() => getWithAuth(`/branch/branchData/${encodeURIComponent(branchName)}`, { projectId }));
|
156 | }
|
157 |
|
158 | function getUserDetails(userId) {
|
159 | return utils.runWithRetries(() => getWithAuth(`/user/user/${userId}`));
|
160 | }
|
161 |
|
162 | function getTestPlanTestList(projectId, names, branch) {
|
163 | return utils.runWithRetries(() => postAuth({
|
164 | url: '/testPlan/list',
|
165 | body: { projectId, names, branch },
|
166 | }));
|
167 | }
|
168 |
|
169 | function getSuiteTestList(projectId, labels, testIds, names, testConfigNames, suites, branch, rerunFailedByRunId, testConfigIds) {
|
170 | return utils.runWithRetries(() => postAuth({
|
171 | url: '/suite/v2/list',
|
172 | body: {
|
173 | projectId,
|
174 | labels,
|
175 | testIds,
|
176 | names,
|
177 | testConfigNames,
|
178 | suites,
|
179 | branch,
|
180 | rerunFailedByRunId,
|
181 | testConfigIds,
|
182 | },
|
183 | }));
|
184 | }
|
185 |
|
186 | function getCurrentPeriodTestExecutionMongo(projectId, plan) {
|
187 | function getBilingStartDate(subscriptionId, projectId) {
|
188 | return utils.runWithRetries(() => getTextWithAuth('/plan/project/billing-period-start-date', {
|
189 | subscriptionId,
|
190 | projectId,
|
191 | }))
|
192 | .then(billingPeriod => (Number(billingPeriod)));
|
193 | }
|
194 |
|
195 | function count(projectId, startTime) {
|
196 | return utils.runWithRetries(() => getWithAuth('/executions/v2/project/count', { projectId, startTime }))
|
197 | .catch(() => ({}));
|
198 | }
|
199 |
|
200 | return getBilingStartDate((plan || {}).subscriptionId, projectId).then(billingPeriod => count(projectId, billingPeriod));
|
201 | }
|
202 |
|
203 | function getOwnerUserIdMongo(projectId) {
|
204 | return utils.runWithRetries(() => getWithAuth('/v2/project/owner', { projectId }))
|
205 | .then(ownerUser => (ownerUser || {}).uid);
|
206 | }
|
207 |
|
208 | function getProjectPlanMongo(projectId) {
|
209 | return utils.runWithRetries(() => getWithAuth('/plan/current-plan', { projectId }));
|
210 | }
|
211 |
|
212 | function getTestResults(testId, resultId, projectId, branch) {
|
213 | return utils.runWithRetries(() => getWithAuth(`/test/v2/${testId}/result/${resultId}`, { projectId, branch }));
|
214 | }
|
215 |
|
216 | function keepAliveGrid(projectId, slots) {
|
217 | return postAuth({
|
218 | url: '/grid/keep-alive',
|
219 | body: { projectId, slots },
|
220 | timeout: 10000,
|
221 | });
|
222 | }
|
223 |
|
224 | function releaseGridSlot(projectId, slotId, gridId, browser) {
|
225 | return postAuth({
|
226 | url: '/grid/release',
|
227 | body: {
|
228 | projectId, slotId, gridId, browser,
|
229 | },
|
230 | });
|
231 | }
|
232 |
|
233 | function getGridByProject(projectId, gridName, browser, executionId) {
|
234 | return utils.runWithRetries(() => getWithAuth('/grid/name', {
|
235 | projectId, name: gridName, browser, executionId,
|
236 | }));
|
237 | }
|
238 |
|
239 | function getGridById(projectId, gridId, browser, executionId) {
|
240 | return utils.runWithRetries(() => getWithAuth(`/grid/${gridId}`, { projectId, browser, executionId }));
|
241 | }
|
242 |
|
243 | function getCompanyByProjectId(projectId) {
|
244 | return utils.runWithRetries(() => getWithAuth(`/company/company/${projectId}`));
|
245 | }
|
246 |
|
247 | function getProject(projectId) {
|
248 | return utils.runWithRetries(() => getWithAuth(`/v2/project/project?projectId=${projectId}`));
|
249 | }
|
250 |
|
251 | async function initializeUserWithAuth({ projectId, token, branchName }) {
|
252 | try {
|
253 | return await utils.runWithRetries(() => httpRequest.post({
|
254 | url: `${config.SERVICES_HOST}/executions/initialize`,
|
255 | body: {
|
256 | projectId,
|
257 | token,
|
258 | branchName: branchName || 'master',
|
259 | },
|
260 | }));
|
261 | } catch (e) {
|
262 | logger.error('error initializing info from server', e);
|
263 | if (e && e.message && e.message.includes('Bad Request')) {
|
264 | throw new ArgError(
|
265 | 'Error trying to retrieve CLI token. ' +
|
266 | 'Your CLI token and project might not match. ' +
|
267 | 'Please make sure to pass `--project` and `--token` that' +
|
268 | ' match to each other or make sure they match in your ~/.testim file.');
|
269 | }
|
270 | if (e && e.code && e.code.includes('ENOTFOUND')) {
|
271 | throw new ArgError('Due to network connectivity issues, Testim CLI has been unable to connect to the Testim backend.');
|
272 | }
|
273 | throw e;
|
274 | }
|
275 | }
|
276 |
|
277 | async function getEditorUrl() {
|
278 | if (config.EDITOR_URL) {
|
279 | return config.EDITOR_URL;
|
280 | }
|
281 | try {
|
282 | return await utils.runWithRetries(() => getWithAuth('/system-info/editor-url'));
|
283 | } catch (err) {
|
284 | logger.error('cannot retrieve editor-url from server');
|
285 | return 'https://app.testim.io';
|
286 | }
|
287 | }
|
288 |
|
289 | function getAllGrids(companyId) {
|
290 | return utils.runWithRetries(() => getWithAuth('/grid', { companyId }));
|
291 | }
|
292 |
|
293 | function getAppUploadUrl(projectId, projectArn, appName) {
|
294 | return utils.runWithRetries(() => postAuth({
|
295 | url: '/deviceFarm/app',
|
296 | body: {
|
297 | projectId,
|
298 | projectArn,
|
299 | appName,
|
300 | },
|
301 | }));
|
302 | }
|
303 |
|
304 | function createDeviceFarmRun(companyId, projectId, projectArn, appArn, configuration, branch, executionId, env, remoteRunObject) {
|
305 | return postAuth({
|
306 | url: '/deviceFarm/run',
|
307 | body: {
|
308 | companyId,
|
309 | projectId,
|
310 | projectArn,
|
311 | appArn,
|
312 | configuration,
|
313 | branch,
|
314 | executionId,
|
315 | env,
|
316 | remoteRunObject,
|
317 | },
|
318 | timeout: 30000,
|
319 | });
|
320 | }
|
321 |
|
322 | function getDeviceFarmRun(projectId, runArn) {
|
323 | return getWithAuth(`/deviceFarm/run?projectId=${projectId}&runArn=${runArn}`);
|
324 | }
|
325 |
|
326 | function getRealData(projectId, channel, query) {
|
327 | return utils.runWithRetries(() => getWithAuth(`/real-data/${channel}?${query}&projectId=${projectId}`));
|
328 | }
|
329 |
|
330 | function updateTestResult(projectId, resultId, testId, testResult, remoteRunId) {
|
331 | return utils.runWithRetries(() => postAuth({
|
332 | url: '/result/test',
|
333 | body: {
|
334 | projectId,
|
335 | resultId,
|
336 | testId,
|
337 | testResult,
|
338 | remoteRunId,
|
339 | },
|
340 | }));
|
341 | }
|
342 |
|
343 | function clearTestResult(projectId, resultId, testId, testResult) {
|
344 | return utils.runWithRetries(() => postAuth({
|
345 | url: '/result/test/clear',
|
346 | body: {
|
347 | projectId,
|
348 | resultId,
|
349 | testId,
|
350 | testResult,
|
351 | },
|
352 | }));
|
353 | }
|
354 |
|
355 | function saveRemoteStep(projectId, resultId, stepId, remoteStep) {
|
356 | return utils.runWithRetries(() => postAuth({
|
357 | url: '/remoteStep',
|
358 | body: {
|
359 | projectId,
|
360 | resultId,
|
361 | stepId,
|
362 | remoteStep,
|
363 | },
|
364 | }));
|
365 | }
|
366 |
|
367 | function relativize(uri) {
|
368 | return uri.startsWith('/') ? uri : `/${uri}`;
|
369 | }
|
370 |
|
371 | function getStorageRelativePath(filePath, bucket, projectId) {
|
372 | let fullPath = relativize(filePath);
|
373 | if (projectId) {
|
374 | fullPath = `/${projectId}${fullPath}`;
|
375 | }
|
376 | if (bucket) {
|
377 | fullPath = `/${bucket}${fullPath}`;
|
378 | }
|
379 |
|
380 | return fullPath;
|
381 | }
|
382 |
|
383 | function uploadArtifact(projectId, testId, testResultId, content, subType, mimeType = 'application/octet-stream') {
|
384 | let fileSuffix = null;
|
385 | if (mimeType === 'application/json') {
|
386 | fileSuffix = '.json';
|
387 | }
|
388 | const fileName = `${subType}_${utils.guid()}${fileSuffix || ''}`;
|
389 | const path = `${testId}/${testResultId}/${fileName}`;
|
390 | const storagePath = getStorageRelativePath(path, 'test-result-artifacts', projectId);
|
391 |
|
392 | const buffer = Buffer.from(pako.gzip(content, {
|
393 | level: 3,
|
394 | }));
|
395 |
|
396 | const files = {
|
397 | file: {
|
398 | fileName,
|
399 | buffer,
|
400 | },
|
401 | };
|
402 |
|
403 | return utils.runWithRetries(() => postAuthFormData(`/storage${storagePath}`, {}, files, { 'Content-Encoding': 'gzip' }))
|
404 | .then(() => storagePath);
|
405 | }
|
406 |
|
407 | const uploadRunDataArtifact = _.memoize(async (projectId, testId, testResultId, runData) => {
|
408 | if (_.isEmpty(runData)) {
|
409 | return undefined;
|
410 | }
|
411 | return await uploadArtifact(projectId, testId, testResultId, JSON.stringify(runData), 'test-run-data', 'application/json');
|
412 | }, (projectId, testId, testResultId, runData) => `${hash(runData)}:${testId}:${testResultId}`);
|
413 |
|
414 | const updateTestDataArtifact = _.memoize(async (projectId, testId, testResultId, testData, projectDefaults) => {
|
415 | if (_.isEmpty(testData)) {
|
416 | return undefined;
|
417 | }
|
418 | const removeHiddenParamsInTestData = () => {
|
419 | const testDataValueClone = _.clone(testData);
|
420 | if (projectDefaults && projectDefaults.hiddenParams) {
|
421 | const { hiddenParams } = projectDefaults;
|
422 | (hiddenParams || []).forEach((param) => {
|
423 | if (testDataValueClone[param]) {
|
424 | testDataValueClone[param] = constants.test.HIDDEN_PARAM;
|
425 | }
|
426 | });
|
427 | }
|
428 | return testDataValueClone;
|
429 | };
|
430 |
|
431 | return await uploadArtifact(projectId, testId, testResultId, JSON.stringify(removeHiddenParamsInTestData(testData)), 'test-test-data', 'application/json');
|
432 | }, (projectId, testId, testResultId, testData) => `${hash(testData)}:${testId}:${testResultId}`);
|
433 |
|
434 | module.exports = {
|
435 | getS3Artifact,
|
436 | getTestPlan,
|
437 | saveTestPlanResult,
|
438 | updateTestStatus,
|
439 | updateExecutionTests,
|
440 | reportExecutionStarted,
|
441 | reportExecutionFinished,
|
442 | getBranchData,
|
443 | getUserDetails,
|
444 | getTestPlanTestList,
|
445 | getSuiteTestList,
|
446 | getCompanyByProjectId,
|
447 | getProject,
|
448 | getOwnerUserIdMongo,
|
449 | getProjectPlanMongo,
|
450 | getCurrentPeriodTestExecutionMongo,
|
451 | getTestResults,
|
452 | getGridByProject,
|
453 | releaseGridSlot,
|
454 | keepAliveGrid,
|
455 | getGridById,
|
456 | getAllGrids,
|
457 | getAppUploadUrl,
|
458 | createDeviceFarmRun,
|
459 | getDeviceFarmRun,
|
460 | getRealData,
|
461 | updateTestResult,
|
462 | clearTestResult,
|
463 | saveRemoteStep,
|
464 | getEditorUrl,
|
465 | uploadRunDataArtifact: Promise.method(uploadRunDataArtifact),
|
466 | updateTestDataArtifact: Promise.method(updateTestDataArtifact),
|
467 | initializeUserWithAuth: Promise.method(initializeUserWithAuth),
|
468 | };
|