From f62d673ae8a8c3ea13e40d3b7c4e7562ff0f1456 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Thu, 2 May 2024 14:22:47 +0200 Subject: [PATCH] techdocs-node: move DockerContainerRunner tests Signed-off-by: Vincenzo Scamporlino --- .../generate/DockerContainerRunner.test.ts | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 plugins/techdocs-node/src/stages/generate/DockerContainerRunner.test.ts diff --git a/plugins/techdocs-node/src/stages/generate/DockerContainerRunner.test.ts b/plugins/techdocs-node/src/stages/generate/DockerContainerRunner.test.ts new file mode 100644 index 0000000000..23536f8c50 --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/DockerContainerRunner.test.ts @@ -0,0 +1,214 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import Docker from 'dockerode'; +import Stream, { PassThrough } from 'stream'; +import { DockerContainerRunner, UserOptions } from './DockerContainerRunner'; +import { createMockDirectory } from '@backstage/backend-test-utils'; +import { ContainerRunner } from './types'; + +const mockDocker = new Docker() as jest.Mocked; + +describe('DockerContainerRunner', () => { + let containerTaskApi: ContainerRunner; + + const inputDir = createMockDirectory(); + const outputDir = createMockDirectory(); + + beforeEach(() => { + inputDir.clear(); + outputDir.clear(); + + jest.spyOn(mockDocker, 'pull').mockImplementation((async ( + _image: string, + _something: any, + handler: (err: Error | undefined, stream: PassThrough) => void, + ) => { + const mockStream = new PassThrough(); + handler(undefined, mockStream); + mockStream.end(); + }) as any); + + jest + .spyOn(mockDocker, 'run') + .mockResolvedValue([{ Error: null, StatusCode: 0 }]); + + jest + .spyOn(mockDocker, 'ping') + .mockResolvedValue(Buffer.from('OK', 'utf-8')); + + containerTaskApi = new DockerContainerRunner(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const imageName = 'dockerOrg/image'; + const args = ['bash', '-c', 'echo test']; + const mountDirs = { + [inputDir.path]: '/input', + [outputDir.path]: '/output', + }; + const workingDir = inputDir.path; + const envVars = { HOME: '/tmp', LOG_LEVEL: 'debug' }; + const envVarsArray = ['HOME=/tmp', 'LOG_LEVEL=debug']; + + it('should pull the docker container', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + }); + + expect(mockDocker.pull).toHaveBeenCalledWith( + imageName, + {}, + expect.any(Function), + ); + + expect(mockDocker.run).toHaveBeenCalled(); + }); + + it('should not pull the docker container when pullImage is false', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + pullImage: false, + }); + + expect(mockDocker.pull).not.toHaveBeenCalled(); + expect(mockDocker.run).toHaveBeenCalled(); + }); + + it('should call the dockerClient run command with the correct arguments passed through', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + mountDirs, + envVars, + workingDir, + }); + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + expect.any(Stream), + expect.objectContaining({ + Env: envVarsArray, + WorkingDir: workingDir, + HostConfig: { + AutoRemove: true, + Binds: expect.arrayContaining([ + `${await fs.realpath(inputDir.path)}:/input`, + `${await fs.realpath(outputDir.path)}:/output`, + ]), + }, + Volumes: { + '/input': {}, + '/output': {}, + }, + }), + ); + }); + + it('should ping docker to test availability', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + }); + + expect(mockDocker.ping).toHaveBeenCalled(); + }); + + it('should pass through the user and group id from the host machine and set the home dir', async () => { + await containerTaskApi.runContainer({ + imageName, + args, + }); + + const userOptions: UserOptions = {}; + if (process.getuid && process.getgid) { + userOptions.User = `${process.getuid()}:${process.getgid()}`; + } + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + expect.any(Stream), + expect.objectContaining({ + ...userOptions, + }), + ); + }); + + it('throws a correct error if the command fails in docker', async () => { + mockDocker.run.mockResolvedValueOnce([ + { + Error: new Error('Something went wrong with docker'), + StatusCode: 0, + }, + ]); + + await expect( + containerTaskApi.runContainer({ + imageName, + args, + }), + ).rejects.toThrow(/Something went wrong with docker/); + }); + + describe('where docker is unavailable', () => { + const dockerError = 'a docker error'; + + beforeEach(() => { + jest.spyOn(mockDocker, 'ping').mockImplementationOnce(() => { + throw new Error(dockerError); + }); + }); + + it('should throw with a descriptive error message including the docker error message', async () => { + await expect( + containerTaskApi.runContainer({ + imageName, + args, + }), + ).rejects.toThrow(new RegExp(`.+: ${dockerError}`)); + }); + }); + + it('should pass through the log stream to the docker client', async () => { + const logStream = new PassThrough(); + await containerTaskApi.runContainer({ + imageName, + args, + logStream, + }); + + expect(mockDocker.run).toHaveBeenCalledWith( + imageName, + args, + logStream, + expect.objectContaining({ + HostConfig: { + AutoRemove: true, + Binds: [], + }, + Volumes: {}, + }), + ); + }); +});