UNPKG

14.9 kBJavaScriptView Raw
1'use strict';
2
3const pako = require('pako');
4const _ = require('lodash');
5const testimCustomToken = require('./testimCustomToken');
6const constants = require('./constants');
7const Promise = require('bluebird');
8const utils = require('../utils.js');
9const config = require('./config');
10const httpRequest = require('./httpRequest');
11
12const runnerVersion = utils.getRunnerVersion();
13const logger = require('./logger').getLogger('testim service api');
14const hash = require('object-hash');
15const { ArgError } = require('../errors');
16
17const DEFAULT_REQUEST_RETRY = 3;
18
19function 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
29function 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
45function 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
53function putAuth(url, body) {
54 return getTokenHeader()
55 .then(headers => httpRequest.put(`${config.SERVICES_HOST}${url || ''}`, body, headers));
56}
57
58function getTextWithAuth(url, query) {
59 return getTokenHeader()
60 .then(headers => httpRequest.getText(`${config.SERVICES_HOST}${url || ''}`, query, headers));
61}
62
63function getWithAuth(url, query, options) {
64 return getTokenHeader()
65 .then(headers => httpRequest.get(`${config.SERVICES_HOST}${url || ''}`, query, headers, undefined, options));
66}
67
68function getS3Artifact(url) {
69 return utils.runWithRetries(() => getWithAuth(`/storage${url}`, null, { isBinary: true }));
70}
71
72function 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
83function saveTestPlanResult(projectId, testPlanId, result) {
84 return utils.runWithRetries(() => postAuth({ url: '/testPlan/result', body: { projectId, testPlanId, result } }));
85}
86
87function 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
104function 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
117function 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
139function 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
154function getBranchData(projectId, branchName) {
155 return utils.runWithRetries(() => getWithAuth(`/branch/branchData/${encodeURIComponent(branchName)}`, { projectId }));
156}
157
158function getUserDetails(userId) {
159 return utils.runWithRetries(() => getWithAuth(`/user/user/${userId}`));
160}
161
162function getTestPlanTestList(projectId, names, branch) {
163 return utils.runWithRetries(() => postAuth({
164 url: '/testPlan/list',
165 body: { projectId, names, branch },
166 }));
167}
168
169function 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
186function 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
203function getOwnerUserIdMongo(projectId) {
204 return utils.runWithRetries(() => getWithAuth('/v2/project/owner', { projectId }))
205 .then(ownerUser => (ownerUser || {}).uid);
206}
207
208function getProjectPlanMongo(projectId) {
209 return utils.runWithRetries(() => getWithAuth('/plan/current-plan', { projectId }));
210}
211
212function getTestResults(testId, resultId, projectId, branch) {
213 return utils.runWithRetries(() => getWithAuth(`/test/v2/${testId}/result/${resultId}`, { projectId, branch }));
214}
215
216function keepAliveGrid(projectId, slots) {
217 return postAuth({
218 url: '/grid/keep-alive',
219 body: { projectId, slots },
220 timeout: 10000,
221 });
222}
223
224function releaseGridSlot(projectId, slotId, gridId, browser) {
225 return postAuth({
226 url: '/grid/release',
227 body: {
228 projectId, slotId, gridId, browser,
229 },
230 });
231}
232
233function getGridByProject(projectId, gridName, browser, executionId) {
234 return utils.runWithRetries(() => getWithAuth('/grid/name', {
235 projectId, name: gridName, browser, executionId,
236 }));
237}
238
239function getGridById(projectId, gridId, browser, executionId) {
240 return utils.runWithRetries(() => getWithAuth(`/grid/${gridId}`, { projectId, browser, executionId }));
241}
242
243function getCompanyByProjectId(projectId) {
244 return utils.runWithRetries(() => getWithAuth(`/company/company/${projectId}`));
245}
246
247function getProject(projectId) {
248 return utils.runWithRetries(() => getWithAuth(`/v2/project/project?projectId=${projectId}`));
249}
250
251async 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
277async 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
289function getAllGrids(companyId) {
290 return utils.runWithRetries(() => getWithAuth('/grid', { companyId }));
291}
292
293function 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
304function 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
322function getDeviceFarmRun(projectId, runArn) {
323 return getWithAuth(`/deviceFarm/run?projectId=${projectId}&runArn=${runArn}`);
324}
325
326function getRealData(projectId, channel, query) {
327 return utils.runWithRetries(() => getWithAuth(`/real-data/${channel}?${query}&projectId=${projectId}`));
328}
329
330function 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
343function 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
355function 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
367function relativize(uri) {
368 return uri.startsWith('/') ? uri : `/${uri}`;
369}
370
371function 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
383function 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, // sufficient time/size ratio.
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
407const 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
414const 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
434module.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};