diff --git a/__fixtures__/output/empty/.keep b/__fixtures__/output/empty/.keep new file mode 100644 index 0000000..e69de29 diff --git a/__fixtures__/output/happy_path/output.json b/__fixtures__/output/happy_path/output.json new file mode 100644 index 0000000..50e114e --- /dev/null +++ b/__fixtures__/output/happy_path/output.json @@ -0,0 +1 @@ +{"base64_dependency_files":[{"name":"go.mod","content":"bW9kdWxlIGdpdGh1Yi5jb20vZGVwZW5kYWJvdC92Z290ZXN0CgpnbyAxLjEy\nCgpyZXF1aXJlIHJzYy5pby9xciB2MC4xLjAKCg==\n","directory":"/","type":"file","support_file":false,"content_encoding":"utf-8","deleted":false,"operation":"update"},{"name":"go.sum","content":"cnNjLmlvL3FyIHYwLjEuMCBoMTpNL3NBeHNVMko1bWxRNFc4NEJ4Z2EyRWdk\nUXFPYUFsaWlwY2pQbU1VTTVRPQpyc2MuaW8vcXIgdjAuMS4wL2dvLm1vZCBo\nMTpJRit1WmprYjlmcXllRi80dGxCb3lucW1ReFVvUGZXRUtoOTIxY29PdVhz\nPQo=\n","directory":"/","type":"file","support_file":false,"content_encoding":"utf-8","deleted":false,"operation":"update"}],"base_commit_sha":"818a3756444cb0d997a1e11820563105db1c24c4"} \ No newline at end of file diff --git a/__fixtures__/output/malformed/output.json b/__fixtures__/output/malformed/output.json new file mode 100644 index 0000000..ea9e681 --- /dev/null +++ b/__fixtures__/output/malformed/output.json @@ -0,0 +1 @@ +C'est ne pas un json diff --git a/__tests__/container-service.test.ts b/__tests__/container-service.test.ts index 5b0782f..698ad41 100644 --- a/__tests__/container-service.test.ts +++ b/__tests__/container-service.test.ts @@ -31,7 +31,7 @@ describe('ContainerService', () => { Image: 'alpine', AttachStdout: true, AttachStderr: true, - Cmd: ['/bin/sh', '-c'] + Cmd: ['/bin/sh', '-c', 'nosuchccommand'] }) }) diff --git a/__tests__/updater.test.ts b/__tests__/updater.test.ts index 591e312..98fe9a8 100644 --- a/__tests__/updater.test.ts +++ b/__tests__/updater.test.ts @@ -1,5 +1,12 @@ -import {UPDATER_IMAGE_NAME, PROXY_IMAGE_NAME} from '../src/main' import {Updater} from '../src/updater' +import Docker from 'dockerode' +import {ContainerService} from '../src/container-service' +import {ProxyBuilder} from '../src/proxy' + +// We do not need to build actual containers or run updates for this test.) +jest.mock('dockerode') +jest.mock('../src/container-service') +jest.mock('../src/proxy') describe('Updater', () => { const mockApiClient: any = { @@ -7,9 +14,9 @@ describe('Updater', () => { getCredentials: jest.fn(), params: { jobId: 1, - jobToken: process.env.JOB_TOKEN, - credentialsToken: process.env.CREDENTIALS_TOKEN, - dependabotApiUrl: 'http://host.docker.internal:3001' + jobToken: 'job-token', + credentialsToken: 'job-credentials-token', + dependabotApiUrl: 'http://localhost:3001' } } @@ -23,18 +30,163 @@ describe('Updater', () => { 'package-manager': 'npm-and-yarn' } - const updater = new Updater( - UPDATER_IMAGE_NAME, - PROXY_IMAGE_NAME, - mockApiClient, - mockJobDetails, - [] - ) + const mockProxy: any = { + container: { + start: jest.fn() + }, + network: jest.fn(), + networkName: 'mockNetworkName', + url: 'http://localhost', + cert: 'mockCertificate', + shutdown: jest.fn() + } + + const mockContainer: any = { + id: 1 + } + + afterEach(async () => { + jest.clearAllMocks() // Reset any mocked classes + }) + + describe('when there is a happy path update', () => { + const updater = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + mockJobDetails, + [], + '__fixtures__/output/happy_path' + ) + + beforeEach(async () => { + jest + .spyOn(Docker.prototype, 'createContainer') + .mockResolvedValue(mockContainer) + + jest.spyOn(ProxyBuilder.prototype, 'run').mockResolvedValue(mockProxy) + jest.spyOn(ContainerService, 'run').mockImplementation(jest.fn()) + }) + + it('should be successful', async () => { + expect(await updater.runUpdater()).toBe(true) + }) + }) + + describe('when the file fetch container fails', () => { + const updater = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + mockJobDetails, + [], + '__fixtures__/output/happy_path' + ) + + beforeEach(async () => { + jest + .spyOn(Docker.prototype, 'createContainer') + .mockResolvedValue(mockContainer) + + jest.spyOn(ProxyBuilder.prototype, 'run').mockResolvedValue(mockProxy) + + jest + .spyOn(ContainerService, 'run') + .mockImplementationOnce( + jest.fn(async () => + Promise.reject(new Error('First call to container service errored')) + ) + ) + }) + + it('should raise an error', async () => { + await expect(updater.runUpdater()).rejects.toThrow() + }) + }) + + describe('when file updater container fails', () => { + const updater = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + mockJobDetails, + [], + '__fixtures__/output/happy_path' + ) + + beforeEach(async () => { + jest + .spyOn(Docker.prototype, 'createContainer') + .mockResolvedValue(mockContainer) + + jest.spyOn(ProxyBuilder.prototype, 'run').mockResolvedValue(mockProxy) + + jest + .spyOn(ContainerService, 'run') + .mockImplementationOnce(jest.fn()) + .mockImplementationOnce( + jest.fn(async () => + Promise.reject( + new Error('Second call to container service errored') + ) + ) + ) + }) + + it('should raise an error', async () => { + await expect(updater.runUpdater()).rejects.toThrow() + }) + }) + + describe('when the file fetch step results in empty output', () => { + const updater = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + mockJobDetails, + [], + '__fixtures__/output/empty' + ) + + beforeEach(async () => { + jest + .spyOn(Docker.prototype, 'createContainer') + .mockResolvedValue(mockContainer) + + jest.spyOn(ProxyBuilder.prototype, 'run').mockResolvedValue(mockProxy) + + jest.spyOn(ContainerService, 'run').mockImplementation(jest.fn()) + }) + + it('should raise an error', async () => { + await expect(updater.runUpdater()).rejects.toThrow( + new Error('No output.json created by the fetcher container') + ) + }) + }) + + describe('when the file fetch step results in malformed output', () => { + const updater = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + mockJobDetails, + [], + '__fixtures__/output/malformed' + ) + + beforeEach(async () => { + jest + .spyOn(Docker.prototype, 'createContainer') + .mockResolvedValue(mockContainer) + + jest.spyOn(ProxyBuilder.prototype, 'run').mockResolvedValue(mockProxy) + + jest.spyOn(ContainerService, 'run').mockImplementation(jest.fn()) + }) - it('should fetch job details', async () => { - mockApiClient.getJobDetails.mockImplementation(() => { - throw new Error('kaboom') + it('should raise an error', async () => { + await expect(updater.runUpdater()).rejects.toThrow() }) - updater.runUpdater() }) }) diff --git a/src/api-client.ts b/src/api-client.ts index fbd7878..1f639ee 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -41,7 +41,7 @@ export class ApiClient { readonly params: JobParameters ) {} - // We use a static unknown SHA when making a job as complete from the action + // We use a static unknown SHA when marking a job as complete from the action // to remain in parity with the existing runner. UnknownSha = { 'base-commit-sha': 'unknown' diff --git a/src/updater.ts b/src/updater.ts index ec40007..c1e8553 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -24,7 +24,8 @@ export class Updater { private readonly proxyImage: string, private readonly apiClient: ApiClient, private readonly details: JobDetails, - private readonly credentials: Credential[] + private readonly credentials: Credential[], + private readonly outputFolder = 'output/' ) { this.docker = new Docker() } @@ -32,37 +33,24 @@ export class Updater { /** * Execute an update job and report the result to Dependabot API. */ - async runUpdater(): Promise { + async runUpdater(): Promise { + const proxy = await new ProxyBuilder(this.docker, this.proxyImage).run( + this.details, + this.credentials + ) + proxy.container.start() + try { - const proxy = await new ProxyBuilder(this.docker, this.proxyImage).run( - this.details, - this.credentials - ) - proxy.container.start() - - try { - const files = await this.runFileFetcher(proxy) - if (!files) { - core.error(`failed during fetch, skipping updater`) - // TODO: report job runner_error? - return - } - - await this.runFileUpdater(proxy, files) - } catch (e) { - // TODO: report job runner_error? - core.error(`Error ${e}`) - } finally { - await proxy.shutdown() - await this.docker.pruneNetworks() - } - } catch (e) { - // TODO: report job runner_error? - core.error(`Error ${e}`) + const files = await this.runFileFetcher(proxy) + await this.runFileUpdater(proxy, files) + return true + } finally { + await proxy.shutdown() + await this.docker.pruneNetworks() } } - private async runFileFetcher(proxy: Proxy): Promise { + private async runFileFetcher(proxy: Proxy): Promise { const container = await this.createContainer(proxy, 'fetch_files') await ContainerService.storeInput( JOB_INPUT_FILENAME, @@ -79,9 +67,14 @@ export class Updater { await ContainerService.run(container) - const outputPath = path.join(__dirname, '../output/output.json') + const outputPath = path.join( + __dirname, + '../', + this.outputFolder, + 'output.json' + ) if (!fs.existsSync(outputPath)) { - return + throw new Error('No output.json created by the fetcher container') } const fileFetcherSync = fs.readFileSync(outputPath).toString()