From a43f07dc4c0b2df2afdb45cb734bb45df5e0a773 Mon Sep 17 00:00:00 2001 From: Stephen Spellman Date: Tue, 19 Mar 2024 00:00:42 -0400 Subject: [PATCH] synthetics mobile command tests --- src/commands/synthetics/__tests__/api.test.ts | 16 + src/commands/synthetics/__tests__/fixtures.ts | 55 ++- .../synthetics/__tests__/mobile.test.ts | 364 +++++++++--------- src/commands/synthetics/mobile.ts | 4 +- .../synthetics/reporters/appUpload.ts | 22 +- src/commands/synthetics/run-tests-command.ts | 2 +- src/commands/synthetics/run-tests-lib.ts | 3 +- .../synthetics/upload-application-command.ts | 2 +- 8 files changed, 261 insertions(+), 207 deletions(-) diff --git a/src/commands/synthetics/__tests__/api.test.ts b/src/commands/synthetics/__tests__/api.test.ts index 436d63ba2..ed0af0693 100644 --- a/src/commands/synthetics/__tests__/api.test.ts +++ b/src/commands/synthetics/__tests__/api.test.ts @@ -22,6 +22,7 @@ import { MOBILE_PRESIGNED_UPLOAD_PARTS, mockSearchResponse, mockTestTriggerResponse, + APP_UPLOAD_POLL_RESULTS, } from './fixtures' describe('dd-api', () => { @@ -280,6 +281,21 @@ describe('dd-api', () => { spy.mockRestore() }) + test('should poll for app upload validation', async () => { + const mockRequest = jest.fn() + const spy = jest.spyOn(axios, 'create').mockImplementation((() => () => ({data: APP_UPLOAD_POLL_RESULTS})) as any) + const api = apiConstructor(apiConfiguration) + const {pollMobileApplicationUploadResponse} = api + + const jobId = 'jobId' + + const appUploadResult = await pollMobileApplicationUploadResponse(jobId) + + expect(mockRequest).toHaveBeenCalled() + expect(appUploadResult).toEqual(APP_UPLOAD_POLL_RESULTS) + spy.mockRestore() + }) + test('should perform search with expected parameters', async () => { const requestMock = jest.fn(() => ({status: 200, data: {tests: []}})) const spy = jest.spyOn(axios, 'create').mockImplementation((() => requestMock) as any) diff --git a/src/commands/synthetics/__tests__/fixtures.ts b/src/commands/synthetics/__tests__/fixtures.ts index afbebf418..05eaa8be4 100644 --- a/src/commands/synthetics/__tests__/fixtures.ts +++ b/src/commands/synthetics/__tests__/fixtures.ts @@ -17,7 +17,6 @@ import { ExecutionRule, Location, MainReporter, - MobileApplicationVersion, MultiStep, MultiStepsServerResult, MobileApplicationUploadPart, @@ -33,7 +32,12 @@ import { Trigger, UploadApplicationCommandConfig, User, + MobileAppUploadResult, + MobileApplicationUploadPartResponse, + TestWithOverride, + TriggerConfig, } from '../interfaces' +import { AppUploadReporter } from '../reporters/appUpload' import {createInitialSummary} from '../utils/public' const mockUser: User = { @@ -569,16 +573,18 @@ export const getTestPayload = (override?: Partial) => ({ ...override, }) -export const getMobileVersion = (override?: Partial) => ({ - id: '123-abc-456', - application_id: '789-dfg-987', - file_name: 'bla.', - original_file_name: 'test.apk', - is_latest: true, - version_name: 'test version', - created_at: '22-09-2022', - ...override, -}) +export const getTestAndConfigOverride = (appId: string): TestWithOverride => { + const testWithOverride = {test: getMobileTest(), overriddenConfig: getTestPayload()} + testWithOverride.test.options.mobileApplication!.applicationId = appId + + return testWithOverride +} + +export const getTriggerConfig = (appPath?: string, appVersion?: string): TriggerConfig => { + const config = appPath ? {mobileApplicationVersionFilePath: appPath} : {mobileApplicationVersion: appVersion} + + return {id: 'abc', config} +} export const uploadCommandConfig: UploadApplicationCommandConfig = { apiKey: 'foo', @@ -608,3 +614,30 @@ export const MOBILE_PRESIGNED_UPLOAD_PARTS: MobileApplicationUploadPart[] = [ {partNumber: 1, md5: 'md5', blob: Buffer.from('content1')}, {partNumber: 2, md5: 'md5', blob: Buffer.from('content2')}, ] + +export const APP_UPLOAD_POLL_RESULTS: MobileAppUploadResult = { + status: 'complete', + is_valid: true, +} + +export const APP_UPLOAD_SIZE_AND_PARTS = { + appSize: 1000, + parts: MOBILE_PRESIGNED_UPLOAD_PARTS, +} + +export const APP_UPLOAD_PART_RESPONSES: MobileApplicationUploadPartResponse[] = MOBILE_PRESIGNED_UPLOAD_PARTS.map( + (partNumber) => ({ + PartNumber: Number(partNumber), + ETag: 'etag', + }) +) + +export const getMockAppUploadReporter = (): AppUploadReporter => { + const reporter: AppUploadReporter = new AppUploadReporter({} as any) + reporter.start = jest.fn() + reporter.renderProgress = jest.fn() + reporter.reportSuccess = jest.fn() + reporter.reportFailure = jest.fn() + + return reporter +} diff --git a/src/commands/synthetics/__tests__/mobile.test.ts b/src/commands/synthetics/__tests__/mobile.test.ts index 6eb81206b..c2c91e300 100644 --- a/src/commands/synthetics/__tests__/mobile.test.ts +++ b/src/commands/synthetics/__tests__/mobile.test.ts @@ -1,11 +1,13 @@ import fs from 'fs' import path from 'path' -import {EndpointError} from '../api' -import {CiError} from '../errors' +import { EndpointError } from '../api' +import { CiError, CriticalError } from '../errors' +import { TestWithOverride, TriggerConfig } from '../interfaces' import * as mobile from '../mobile' +import { AppUploadReporter } from '../reporters/appUpload' -import {getApiHelper, getMobileTest, getMobileVersion, getTestPayload, uploadCommandConfig} from './fixtures' +import {APP_UPLOAD_POLL_RESULTS, MOBILE_PRESIGNED_URLS_PAYLOAD, getApiHelper, getMobileTest, getTestPayload, APP_UPLOAD_SIZE_AND_PARTS, APP_UPLOAD_PART_RESPONSES, getTriggerConfig, getTestAndConfigOverride, uploadCommandConfig, getMockAppUploadReporter} from './fixtures' describe('getSizeAndPartsFromFile', () => { test('correctly get size and parts of a file', async () => { @@ -33,158 +35,139 @@ describe('getSizeAndPartsFromFile', () => { }) }) -describe('uploadApplication', () => { - const uploadApplicationSpy = jest.spyOn(mobile, 'uploadMobileApplications') +describe('uploadMobileApplication', () => { const api = getApiHelper() + let getSizeAndPartsFromFileSpy: jest.SpyInstance + let getMobileApplicationPresignedURLsSpy: jest.SpyInstance + let uploadMobileApplicationPartSpy: jest.SpyInstance + let completeMultipartMobileApplicationUploadSpy: jest.SpyInstance + let pollMobileApplicationUploadResponseSpy: jest.SpyInstance + const jobId = 'jobId' beforeEach(() => { - uploadApplicationSpy.mockReset() - uploadApplicationSpy.mockImplementation(async () => 'fileName') + getSizeAndPartsFromFileSpy = jest.spyOn(mobile, 'getSizeAndPartsFromFile').mockImplementation(async () => APP_UPLOAD_SIZE_AND_PARTS) + getMobileApplicationPresignedURLsSpy = jest.spyOn(api, 'getMobileApplicationPresignedURLs').mockImplementation(async () => MOBILE_PRESIGNED_URLS_PAYLOAD) + uploadMobileApplicationPartSpy = jest.spyOn(api, 'uploadMobileApplicationPart').mockImplementation(async () => APP_UPLOAD_PART_RESPONSES) + completeMultipartMobileApplicationUploadSpy = jest.spyOn(api, 'completeMultipartMobileApplicationUpload').mockImplementation(async () => jobId) + pollMobileApplicationUploadResponseSpy = jest.spyOn(api, 'pollMobileApplicationUploadResponse').mockImplementation(async () => APP_UPLOAD_POLL_RESULTS) }) - test('upload new application file', async () => { - const uploadedApplicationByPath = {} - await mobile.uploadApplication(api, 'new-application-path.api', 'mobileAppUuid', uploadedApplicationByPath) - - expect(uploadedApplicationByPath).toEqual({ - 'new-application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - }) - expect(uploadApplicationSpy).toHaveBeenCalledTimes(1) + test('happy path', async () => { + const result = await mobile.uploadMobileApplication(api, 'new-application-path.ipa', 'mobileAppUuid') + + expect(getSizeAndPartsFromFileSpy).toHaveBeenCalledWith('new-application-path.ipa') + expect(getMobileApplicationPresignedURLsSpy).toHaveBeenCalledWith('mobileAppUuid', APP_UPLOAD_SIZE_AND_PARTS.appSize, APP_UPLOAD_SIZE_AND_PARTS.parts) + expect(uploadMobileApplicationPartSpy).toHaveBeenCalledWith(APP_UPLOAD_SIZE_AND_PARTS.parts, MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params) + expect(completeMultipartMobileApplicationUploadSpy).toHaveBeenCalledWith('mobileAppUuid', MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params.upload_id, MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params.key, APP_UPLOAD_PART_RESPONSES, undefined) + expect(pollMobileApplicationUploadResponseSpy).toHaveBeenCalledWith(jobId) + expect(result).toEqual({appUploadResponse: APP_UPLOAD_POLL_RESULTS, fileName: MOBILE_PRESIGNED_URLS_PAYLOAD.file_name}) }) - test('upload same application file with different application id', async () => { - const uploadedApplicationByPath = { - 'new-application-path.api': [ - { - applicationId: 'anotherMobileAppUuid', - fileName: 'fileName', - }, - ], + test('happy path with new version params', async () => { + const newVersionParams = { + originalFileName: 'originalFileName', + versionName: 'versionName', + isLatest: true, } - await mobile.uploadApplication(api, 'new-application-path.api', 'mobileAppUuid', uploadedApplicationByPath) - - expect(uploadedApplicationByPath).toEqual({ - 'new-application-path.api': [ - { - applicationId: 'anotherMobileAppUuid', - fileName: 'fileName', - }, - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - }) + const result = await mobile.uploadMobileApplication(api, 'new-application-path.ipa', 'mobileAppUuid', newVersionParams) + + expect(getSizeAndPartsFromFileSpy).toHaveBeenCalledWith('new-application-path.ipa') + expect(getMobileApplicationPresignedURLsSpy).toHaveBeenCalledWith('mobileAppUuid', APP_UPLOAD_SIZE_AND_PARTS.appSize, APP_UPLOAD_SIZE_AND_PARTS.parts) + expect(uploadMobileApplicationPartSpy).toHaveBeenCalledWith(APP_UPLOAD_SIZE_AND_PARTS.parts, MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params) + expect(completeMultipartMobileApplicationUploadSpy).toHaveBeenCalledWith('mobileAppUuid', MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params.upload_id, MOBILE_PRESIGNED_URLS_PAYLOAD.multipart_presigned_urls_params.key, APP_UPLOAD_PART_RESPONSES, newVersionParams) + expect(pollMobileApplicationUploadResponseSpy).toHaveBeenCalledWith(jobId) + expect(result).toEqual({appUploadResponse: APP_UPLOAD_POLL_RESULTS, fileName: MOBILE_PRESIGNED_URLS_PAYLOAD.file_name}) + }) + + test('invalid app throws', async () => { + const appUploadResponse = { + status: 'complete', + is_valid: false, + invalid_app_result: { + invalid_message: 'invalid message', + invalid_reason: 'invalid reason', + }, + } + pollMobileApplicationUploadResponseSpy.mockImplementation(async () => appUploadResponse) + const expectedError = new CiError('INVALID_MOBILE_APP', `Mobile application failed validation for reason: ${appUploadResponse.invalid_app_result.invalid_message}`) - expect(uploadApplicationSpy).toHaveBeenCalledTimes(1) + await expect(mobile.uploadMobileApplication(api, 'invalid-application-path.ipa', 'mobileAppUuid')).rejects.toThrow(expectedError) }) - test('upload different application file with same application id', async () => { - const uploadedApplicationByPath = { - 'new-application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], + test('user error upload throws', async () => { + const appUploadResponse = { + status: 'user_error', + user_error_result: { + user_error_reason: 'user error reason', + user_error_message: 'user error message', + }, } - await mobile.uploadApplication(api, 'another-application-path.api', 'mobileAppUuid', uploadedApplicationByPath) - - expect(uploadedApplicationByPath).toEqual({ - 'another-application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - 'new-application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - }) + pollMobileApplicationUploadResponseSpy.mockImplementation(async () => appUploadResponse) + const expectedError = new CiError('INVALID_MOBILE_APP_UPLOAD_PARAMETERS', `Mobile application failed validation for reason: ${appUploadResponse.user_error_result.user_error_message}`) - expect(uploadApplicationSpy).toHaveBeenCalledTimes(1) + await expect(mobile.uploadMobileApplication(api, 'user-error-application-path.ipa', 'mobileAppUuid')).rejects.toThrow(expectedError) }) - test('upload different application file with different application id', async () => { - const uploadedApplicationByPath = { - 'another-application-path.api': [ - { - applicationId: 'anotherMobileAppUuid', - fileName: 'fileName', - }, - ], + test('user 500 validation error throws', async () => { + const appUploadResponse = { + status: 'error', } - await mobile.uploadApplication(api, 'new-application-path.api', 'mobileAppUuid', uploadedApplicationByPath) - - expect(uploadedApplicationByPath).toEqual({ - 'another-application-path.api': [ - { - applicationId: 'anotherMobileAppUuid', - fileName: 'fileName', - }, - ], - 'new-application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - }) + pollMobileApplicationUploadResponseSpy.mockImplementation(async () => appUploadResponse) + const expectedError = new CriticalError('UNKNOWN_MOBILE_APP_UPLOAD_FAILURE', 'Unknown mobile application upload error.') - expect(uploadApplicationSpy).toHaveBeenCalledTimes(1) + await expect(mobile.uploadMobileApplication(api, 'error-application-path.ipa', 'mobileAppUuid')).rejects.toThrow(expectedError) }) }) -describe('shouldUploadApplication', () => { - test('New application', () => { - expect(mobile.shouldUploadApplication('application-path.api', 'mobileAppUuid', {})).toBe(true) +describe('AppUploadCache', () => { + let triggerConfigs: TriggerConfig[] + let testsAndConfigsOverride: TestWithOverride[] + + beforeEach(() => { + triggerConfigs = [ + getTriggerConfig('appPath1'), + getTriggerConfig('appPath2'), + getTriggerConfig('appPath1'), + getTriggerConfig('appPath3'), + ] + testsAndConfigsOverride = [ + getTestAndConfigOverride('appId1'), + getTestAndConfigOverride('appId2'), + getTestAndConfigOverride('appId1'), + getTestAndConfigOverride('appId3'), + ] }) - test('Application already uploaded', () => { - const uploadedApplicationByPath = { - 'application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - } - expect(mobile.shouldUploadApplication('application-path.api', 'mobileAppUuid', uploadedApplicationByPath)).toBe( - false - ) + test('setAppCacheKeys', () => { + const cache = new mobile.AppUploadCache() + cache.setAppCacheKeys(triggerConfigs, testsAndConfigsOverride) + + expect(cache.getAppsToUpload()).toEqual([ + {appId: 'appId1', appPath: 'appPath1'}, + {appId: 'appId2', appPath: 'appPath2'}, + {appId: 'appId3', appPath: 'appPath3'}, + ]) }) - test('Application already uploaded but with different applicationId', () => { - const uploadedApplicationByPath = { - 'application-path.api': [ - { - applicationId: 'mobileAppUuid', - fileName: 'fileName', - }, - ], - } - expect(mobile.shouldUploadApplication('application-path.api', 'newMobileAppUuid', uploadedApplicationByPath)).toBe( - true - ) + test('setFileName', () => { + const cache = new mobile.AppUploadCache() + cache.setAppCacheKeys(triggerConfigs, testsAndConfigsOverride) + cache.setFileName('appPath1', 'appId1', 'fileName') + + expect(cache.getFileName('appPath1', 'appId1')).toBe('fileName') }) + }) describe('overrideMobileConfig', () => { test('mobileApplicationVersionFilePath', () => { const test = getMobileTest() const overriddenConfig = getTestPayload({public_id: test.public_id}) - mobile.overrideMobileConfig({mobileApplicationVersionFilePath: 'androidAppPath'}, overriddenConfig, test, { - applicationId: test.options.mobileApplication!.applicationId, - fileName: 'fileName', - }) + mobile.overrideMobileConfig( + overriddenConfig, + test.options.mobileApplication!.applicationId, + 'fileName' + ) expect(overriddenConfig.mobileApplication).toEqual({ applicationId: test.options.mobileApplication!.applicationId, @@ -196,7 +179,12 @@ describe('overrideMobileConfig', () => { test('mobileApplicationVersion', () => { const test = getMobileTest() const overriddenConfig = getTestPayload({public_id: test.public_id}) - mobile.overrideMobileConfig({mobileApplicationVersion: 'newAndroidVersionId'}, overriddenConfig, test) + mobile.overrideMobileConfig( + overriddenConfig, + test.options.mobileApplication!.applicationId, + undefined, + 'newAndroidVersionId' + ) expect(overriddenConfig.mobileApplication).toEqual({ applicationId: test.options.mobileApplication!.applicationId, @@ -205,20 +193,14 @@ describe('overrideMobileConfig', () => { }) }) - test('Path takes precedence over version', () => { + test('Temporary takes precedence over version', () => { const test = getMobileTest() const overriddenConfig = getTestPayload({public_id: test.public_id}) mobile.overrideMobileConfig( - { - mobileApplicationVersion: 'androidVersionId', - mobileApplicationVersionFilePath: 'androidAppPath', - }, overriddenConfig, - getMobileTest(), - { - applicationId: test.options.mobileApplication!.applicationId, - fileName: 'fileName', - } + test.options.mobileApplication!.applicationId, + 'fileName', + 'androidVersionId' ) expect(overriddenConfig.mobileApplication).toEqual({ @@ -229,78 +211,103 @@ describe('overrideMobileConfig', () => { }) }) -describe('uploadApplicationAndOverrideConfig', () => { - const uploadApplicationSpy = jest.spyOn(mobile, 'uploadMobileApplications') +describe('uploadMobileApplicationsAndOverrideConfigs', () => { const api = getApiHelper() + const triggerConfigs = [ + getTriggerConfig('appPath1'), + getTriggerConfig('appPath2'), + getTriggerConfig('appPath1'), + getTriggerConfig('appPath3'), + getTriggerConfig(undefined, 'appVersion1'), + ] + const testsAndConfigsOverride = [ + getTestAndConfigOverride('appId1'), + getTestAndConfigOverride('appId2'), + getTestAndConfigOverride('appId1'), + getTestAndConfigOverride('appId3'), + getTestAndConfigOverride('appId4'), + ] + let appUploadReporter: AppUploadReporter + + const uploadMobileApplicationSpy = jest.spyOn(mobile, 'uploadMobileApplication') + const overrideMobileConfigSpy = jest.spyOn(mobile, 'overrideMobileConfig') beforeEach(() => { - uploadApplicationSpy.mockReset() - uploadApplicationSpy.mockImplementation(async () => 'fileName') + appUploadReporter = getMockAppUploadReporter() + uploadMobileApplicationSpy.mockReset() + overrideMobileConfigSpy.mockReset() }) - test('Upload and override for mobile tests', async () => { - const uploadedApplicationByPath: {[applicationFilePath: string]: {applicationId: string; fileName: string}[]} = {} - const mobileTest = getMobileTest() - const mobileTestConfig = getTestPayload({public_id: mobileTest.public_id}) - await mobile.uploadApplicationAndOverrideConfig( - api, - mobileTest, - {mobileApplicationVersionFilePath: 'androidAppPath'}, - mobileTestConfig, - uploadedApplicationByPath - ) - - expect(mobileTestConfig.mobileApplication).toEqual({ - applicationId: 'mobileAppUuid', - referenceId: 'fileName', - referenceType: 'temporary', + test('happy path', async () => { + uploadMobileApplicationSpy.mockImplementation(async (_, __, appId) => { + return {fileName: `fileName-${appId}`, appUploadResponse: APP_UPLOAD_POLL_RESULTS} }) - await mobile.uploadApplicationAndOverrideConfig( - api, - mobileTest, - {mobileApplicationVersion: 'androidAppVersion'}, - mobileTestConfig, - uploadedApplicationByPath - ) + await mobile.uploadMobileApplicationsAndOverrideConfigs(api, triggerConfigs, testsAndConfigsOverride, appUploadReporter) + + expect(appUploadReporter.start).toHaveBeenCalledWith([ + {appId: 'appId1', appPath: 'appPath1'}, + {appId: 'appId2', appPath: 'appPath2'}, + {appId: 'appId3', appPath: 'appPath3'}, + ]) + expect(appUploadReporter.renderProgress).toHaveBeenCalledTimes(3) + expect(appUploadReporter.reportSuccess).toHaveBeenCalledTimes(1) + expect(overrideMobileConfigSpy).toHaveBeenCalledTimes(5) + expect(overrideMobileConfigSpy.mock.calls).toEqual([ + [testsAndConfigsOverride[0].overriddenConfig, 'appId1', 'fileName-appId1', undefined], + [testsAndConfigsOverride[1].overriddenConfig, 'appId2', 'fileName-appId2', undefined], + [testsAndConfigsOverride[2].overriddenConfig, 'appId1', 'fileName-appId1', undefined], + [testsAndConfigsOverride[3].overriddenConfig, 'appId3', 'fileName-appId3', undefined], + [testsAndConfigsOverride[4].overriddenConfig, 'appId4', undefined, triggerConfigs[4].config.mobileApplicationVersion], + ]) + }) - expect(mobileTestConfig.mobileApplication).toEqual({ - applicationId: 'mobileAppUuid', - referenceId: 'androidAppVersion', - referenceType: 'version', + test('upload failure', async () => { + uploadMobileApplicationSpy.mockImplementation(async () => { + throw new Error('mock failure') }) + + await expect(mobile.uploadMobileApplicationsAndOverrideConfigs(api, triggerConfigs, testsAndConfigsOverride, appUploadReporter)).rejects.toThrow(Error) + + expect(appUploadReporter.reportFailure).toHaveBeenCalledTimes(1) + expect(appUploadReporter.reportSuccess).toHaveBeenCalledTimes(0) + expect(overrideMobileConfigSpy).toHaveBeenCalledTimes(0) }) }) describe('uploadMobileApplicationVersion', () => { - const uploadMobileApplicationSpy = jest.spyOn(mobile, 'uploadMobileApplications') - const createNewMobileVersionSpy = jest.spyOn(mobile, 'createNewMobileVersion') + const uploadMobileApplicationSpy = jest.spyOn(mobile, 'uploadMobileApplication') const config = uploadCommandConfig beforeEach(() => { uploadMobileApplicationSpy.mockReset() - createNewMobileVersionSpy.mockReset() }) test('upload new application file', async () => { - uploadMobileApplicationSpy.mockImplementation(async () => 'abc-123') - createNewMobileVersionSpy.mockImplementation(async () => getMobileVersion({id: 'def-456'})) + uploadMobileApplicationSpy.mockImplementation(async () => { + return {fileName: 'abc-123', appUploadResponse: APP_UPLOAD_POLL_RESULTS} + }) await mobile.uploadMobileApplicationVersion(config) - expect(uploadMobileApplicationSpy).toHaveBeenCalledTimes(1) - const callArg = createNewMobileVersionSpy.mock.calls[0][1] - expect(callArg.file_name).toBe('abc-123') + expect(uploadMobileApplicationSpy).toHaveBeenCalledWith( + expect.anything(), + uploadCommandConfig.mobileApplicationVersionFilePath, + uploadCommandConfig.mobileApplicationId, + { + originalFileName: uploadCommandConfig.mobileApplicationVersionFilePath, + versionName: uploadCommandConfig.versionName, + isLatest: uploadCommandConfig.latest, + } + ) }) - test('get pre-signe URL fails', async () => { + test('get pre-signed URL fails', async () => { uploadMobileApplicationSpy.mockImplementation(() => { throw new EndpointError('mock fail', 1) }) await expect(mobile.uploadMobileApplicationVersion(config)).rejects.toThrow(EndpointError) - - expect(createNewMobileVersionSpy).toHaveBeenCalledTimes(0) }) test('missing mobile application ID', async () => { @@ -308,7 +315,6 @@ describe('uploadMobileApplicationVersion', () => { await expect(mobile.uploadMobileApplicationVersion(config)).rejects.toThrow(CiError) expect(uploadMobileApplicationSpy).toHaveBeenCalledTimes(0) - expect(createNewMobileVersionSpy).toHaveBeenCalledTimes(0) }) test('missing mobile application file', async () => { @@ -316,7 +322,6 @@ describe('uploadMobileApplicationVersion', () => { await expect(mobile.uploadMobileApplicationVersion(config)).rejects.toThrow(CiError) expect(uploadMobileApplicationSpy).toHaveBeenCalledTimes(0) - expect(createNewMobileVersionSpy).toHaveBeenCalledTimes(0) }) test('missing version name', async () => { @@ -324,6 +329,5 @@ describe('uploadMobileApplicationVersion', () => { await expect(mobile.uploadMobileApplicationVersion(config)).rejects.toThrow(CiError) expect(uploadMobileApplicationSpy).toHaveBeenCalledTimes(0) - expect(createNewMobileVersionSpy).toHaveBeenCalledTimes(0) }) }) diff --git a/src/commands/synthetics/mobile.ts b/src/commands/synthetics/mobile.ts index 4ae860f26..6c53e1935 100644 --- a/src/commands/synthetics/mobile.ts +++ b/src/commands/synthetics/mobile.ts @@ -21,8 +21,8 @@ import { AppUploadReporter } from './reporters/appUpload' import { wait } from './utils/public' const UPLOAD_FILE_MAX_PART_SIZE = 10 * 1024 * 1024 // MiB -const APP_UPLOAD_POLLING_INTERVAL = 1000 // 1 second -const MAX_APP_UPLOAD_POLLING_TIMEOUT = 5 * 60 * 1000 // 5 minutes +export const APP_UPLOAD_POLLING_INTERVAL = 1000 // 1 second +export const MAX_APP_UPLOAD_POLLING_TIMEOUT = 5 * 60 * 1000 // 5 minutes export const getSizeAndPartsFromFile = async ( filePath: string diff --git a/src/commands/synthetics/reporters/appUpload.ts b/src/commands/synthetics/reporters/appUpload.ts index 175af75d3..5378a1b45 100644 --- a/src/commands/synthetics/reporters/appUpload.ts +++ b/src/commands/synthetics/reporters/appUpload.ts @@ -1,5 +1,3 @@ -import type {Writable} from 'stream' - import chalk from 'chalk' import ora from 'ora' @@ -13,20 +11,18 @@ export class AppUploadReporter { private context: CommandContext private spinner?: ora.Ora private startTime: number - private write: Writable['write'] - constructor({context}: {context: CommandContext}) { + constructor(context: CommandContext) { this.context = context - this.write = context.stdout.write.bind(context.stdout) this.startTime = Date.now() } - public start(appsToUpload: AppUploadDetails[]) { + public start = (appsToUpload: AppUploadDetails[]): void => { this.write(`\n${appsToUpload.length} mobile application(s) to upload:\n`) this.write(appsToUpload.map((appToUpload) => this.getAppRepr(appToUpload)).join('\n') + '\n') } - public renderProgress(numberOfApplicationsLeft: number) { + public renderProgress = (numberOfApplicationsLeft: number): void => { const text = `Uploading ${numberOfApplicationsLeft} application(s)…` this.spinner?.stop() this.spinner = ora({ @@ -36,23 +32,23 @@ export class AppUploadReporter { this.spinner.start() } - public reportSuccess() { + public reportSuccess = (): void => { this.endRendering() this.write(`${ICONS.SUCCESS} Uploaded applications in ${Math.round((Date.now() - this.startTime)/1000)}s`) } - public reportFailure(error: Error, failedApp: AppUploadDetails) { + public reportFailure = (error: Error, failedApp: AppUploadDetails): void => { this.endRendering() this.write(`${ICONS.FAILED} Failed to upload application:\n${this.getAppRepr(failedApp)}\n`) this.write(`${chalk.red(error.message)}\n`) } - public endRendering() { + public endRendering = (): void => { this.spinner?.stop() delete this.spinner } - private getAppRepr(appUploadDetails: AppUploadDetails) { + private getAppRepr = (appUploadDetails: AppUploadDetails): string => { let versionPrepend = '' if (appUploadDetails.versionName) { versionPrepend = `Version ${chalk.dim.cyan(appUploadDetails.versionName)} - ` @@ -60,4 +56,8 @@ export class AppUploadReporter { return ` ${versionPrepend}Application ID ${chalk.dim.cyan(appUploadDetails.appId)} - Local Path ${chalk.dim.cyan(appUploadDetails.appPath)}` } + + private write = (message: string): void => { + this.context.stdout.write(message) + } } diff --git a/src/commands/synthetics/run-tests-command.ts b/src/commands/synthetics/run-tests-command.ts index 907bad9ca..fc0df2708 100644 --- a/src/commands/synthetics/run-tests-command.ts +++ b/src/commands/synthetics/run-tests-command.ts @@ -165,7 +165,7 @@ export class RunTestsCommand extends Command { let results: Result[] let summary: Summary - const appUploadReporter = new AppUploadReporter(this) + const appUploadReporter = new AppUploadReporter(this.context) try { ;({results, summary} = await executeTests(this.reporter, appUploadReporter, this.config)) } catch (error) { diff --git a/src/commands/synthetics/run-tests-lib.ts b/src/commands/synthetics/run-tests-lib.ts index accf9b7d9..22696b6bd 100644 --- a/src/commands/synthetics/run-tests-lib.ts +++ b/src/commands/synthetics/run-tests-lib.ts @@ -310,7 +310,8 @@ export const executeWithDetails = async ( } const mainReporter = getReporter(localReporters) - const {results, summary} = await executeTests(mainReporter, localConfig, suites) + const appUploadReporter = new AppUploadReporter(process) + const {results, summary} = await executeTests(mainReporter, appUploadReporter, localConfig, suites) const orgSettings = await getOrgSettings(mainReporter, localConfig) diff --git a/src/commands/synthetics/upload-application-command.ts b/src/commands/synthetics/upload-application-command.ts index 9928e0485..e0787be6c 100644 --- a/src/commands/synthetics/upload-application-command.ts +++ b/src/commands/synthetics/upload-application-command.ts @@ -75,7 +75,7 @@ export class UploadApplicationCommand extends Command { return 1 } - const appUploadReporter = new AppUploadReporter({context: this.context}) + const appUploadReporter = new AppUploadReporter(this.context) const appRenderingInfo = { appId: this.config.mobileApplicationId!, appPath: this.config.mobileApplicationVersionFilePath!,