|
| 1 | +/* eslint-disable chai-friendly/no-unused-expressions */ |
| 2 | +import Nanobus from 'nanobus'; |
| 3 | +import { promises as fs } from 'fs'; |
| 4 | +import path from 'path'; |
| 5 | +import { promisify } from 'util'; |
| 6 | +import { getMongocryptdPaths, MongocryptdManager } from './mongocryptd-manager'; |
| 7 | +import type { MongoshBus } from '@mongosh/types'; |
| 8 | +import { ShellHomeDirectory } from './config-directory'; |
| 9 | +import { startTestServer } from '../../../testing/integration-testing-hooks'; |
| 10 | +import { expect } from 'chai'; |
| 11 | + |
| 12 | +describe('getMongocryptdPaths', () => { |
| 13 | + it('always includes plain `mongocryptd`', async() => { |
| 14 | + expect(await getMongocryptdPaths()).to.deep.include(['mongocryptd']); |
| 15 | + }); |
| 16 | +}); |
| 17 | + |
| 18 | +describe('MongocryptdManager', () => { |
| 19 | + let basePath: string; |
| 20 | + let bus: MongoshBus; |
| 21 | + let shellHomeDirectory: ShellHomeDirectory; |
| 22 | + let spawnPaths: string[][]; |
| 23 | + let manager: MongocryptdManager; |
| 24 | + let events: { event: string, data: any }[]; |
| 25 | + const makeManager = () => { |
| 26 | + manager = new MongocryptdManager(spawnPaths, shellHomeDirectory, bus); |
| 27 | + return manager; |
| 28 | + }; |
| 29 | + |
| 30 | + const fakeMongocryptdDir = path.resolve(__dirname, '..', 'test', 'fixtures', 'fake-mongocryptd'); |
| 31 | + |
| 32 | + beforeEach(() => { |
| 33 | + const nanobus = new Nanobus(); |
| 34 | + events = []; |
| 35 | + nanobus.on('*', (event, data) => events.push({ event, data })); |
| 36 | + bus = nanobus; |
| 37 | + |
| 38 | + spawnPaths = []; |
| 39 | + basePath = path.resolve(__dirname, '..', '..', '..', 'tmp', 'test', `${Date.now()}`, `${Math.random()}`); |
| 40 | + shellHomeDirectory = new ShellHomeDirectory({ |
| 41 | + shellRoamingDataPath: basePath, |
| 42 | + shellLocalDataPath: basePath, |
| 43 | + shellRcPath: basePath |
| 44 | + }); |
| 45 | + }); |
| 46 | + afterEach(() => { |
| 47 | + manager?.close(); |
| 48 | + }); |
| 49 | + |
| 50 | + it('does a no-op close when not initialized', () => { |
| 51 | + expect(makeManager().close().state).to.equal(null); |
| 52 | + }); |
| 53 | + |
| 54 | + for (const otherMongocryptd of ['none', 'missing', 'broken', 'weirdlog']) { |
| 55 | + for (const version of ['4.2', '4.4']) { |
| 56 | + for (const variant of ['withunix', 'nounix']) { |
| 57 | + // eslint-disable-next-line no-loop-func |
| 58 | + it(`spawns a working mongocryptd (${version}, ${variant}, other mongocryptd: ${otherMongocryptd})`, async() => { |
| 59 | + spawnPaths = [ |
| 60 | + [ |
| 61 | + process.execPath, |
| 62 | + path.resolve(fakeMongocryptdDir, `working-${version}-${variant}.js`) |
| 63 | + ] |
| 64 | + ]; |
| 65 | + if (otherMongocryptd === 'missing') { |
| 66 | + spawnPaths.unshift([ path.resolve(fakeMongocryptdDir, 'nonexistent') ]); |
| 67 | + } |
| 68 | + if (otherMongocryptd === 'broken') { |
| 69 | + spawnPaths.unshift([ process.execPath, path.resolve(fakeMongocryptdDir, 'exit1') ]); |
| 70 | + } |
| 71 | + if (otherMongocryptd === 'weirdlog') { |
| 72 | + spawnPaths.unshift([ process.execPath, path.resolve(fakeMongocryptdDir, 'weirdlog') ]); |
| 73 | + } |
| 74 | + expect(await makeManager().start()).to.deep.equal({ |
| 75 | + mongocryptdURI: variant === 'nounix' ? |
| 76 | + 'mongodb://localhost:27020' : |
| 77 | + 'mongodb://%2Ftmp%2Fmongocryptd.sock', |
| 78 | + mongocryptdBypassSpawn: true |
| 79 | + }); |
| 80 | + |
| 81 | + const tryspawns = events.filter(({ event }) => event === 'mongosh:mongocryptd-tryspawn'); |
| 82 | + expect(tryspawns).to.have.lengthOf(otherMongocryptd === 'none' ? 1 : 2); |
| 83 | + }); |
| 84 | + } |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + it('passes relevant arguments to mongocryptd', async() => { |
| 89 | + spawnPaths = [[process.execPath, path.resolve(fakeMongocryptdDir, 'writepidfile.js')]]; |
| 90 | + await makeManager().start(); |
| 91 | + const pidfile = path.join(manager.path, 'mongocryptd.pid'); |
| 92 | + expect(JSON.parse(await fs.readFile(pidfile, 'utf8')).args).to.deep.equal([ |
| 93 | + ...spawnPaths[0], |
| 94 | + '--idleShutdownTimeoutSecs', '60', |
| 95 | + '--pidfilepath', pidfile, |
| 96 | + '--port', '0', |
| 97 | + ...(process.platform !== 'win32' ? ['--unixSocketPrefix', path.dirname(pidfile)] : []) |
| 98 | + ]); |
| 99 | + }); |
| 100 | + |
| 101 | + it('multiple start() calls are no-ops', async() => { |
| 102 | + spawnPaths = [[process.execPath, path.resolve(fakeMongocryptdDir, 'writepidfile.js')]]; |
| 103 | + const manager = makeManager(); |
| 104 | + await manager.start(); |
| 105 | + const pid1 = manager.state.proc.pid; |
| 106 | + await manager.start(); |
| 107 | + expect(manager.state.proc.pid).to.equal(pid1); |
| 108 | + }); |
| 109 | + |
| 110 | + it('handles synchronous throws from child_process.spawn', async() => { |
| 111 | + spawnPaths = [['']]; |
| 112 | + try { |
| 113 | + await makeManager().start(); |
| 114 | + expect.fail('missed exception'); |
| 115 | + } catch (e) { |
| 116 | + expect(e.code).to.equal('ERR_INVALID_ARG_VALUE'); |
| 117 | + } |
| 118 | + }); |
| 119 | + |
| 120 | + it('throws if no spawn paths are provided at all', async() => { |
| 121 | + spawnPaths = []; |
| 122 | + try { |
| 123 | + await makeManager().start(); |
| 124 | + expect.fail('missed exception'); |
| 125 | + } catch (e) { |
| 126 | + expect(e.name).to.equal('MongoshInternalError'); |
| 127 | + } |
| 128 | + }); |
| 129 | + |
| 130 | + it('includes stderr in the log if stdout is unparseable', async() => { |
| 131 | + spawnPaths = [[process.execPath, path.resolve(fakeMongocryptdDir, 'weirdlog.js')]]; |
| 132 | + try { |
| 133 | + await makeManager().start(); |
| 134 | + expect.fail('missed exception'); |
| 135 | + } catch (e) { |
| 136 | + expect(e.name).to.equal('MongoshInternalError'); |
| 137 | + } |
| 138 | + const nostdoutErrors = events.filter(({ event, data }) => { |
| 139 | + return event === 'mongosh:mongocryptd-error' && data.cause === 'nostdout'; |
| 140 | + }); |
| 141 | + expect(nostdoutErrors).to.deep.equal([{ |
| 142 | + event: 'mongosh:mongocryptd-error', |
| 143 | + data: { cause: 'nostdout', stderr: 'Diagnostic message!\n' } |
| 144 | + }]); |
| 145 | + }); |
| 146 | + |
| 147 | + it('cleans up previously created, empty directory entries', async() => { |
| 148 | + spawnPaths = [[process.execPath, path.resolve(fakeMongocryptdDir, 'writepidfile.js')]]; |
| 149 | + |
| 150 | + const manager = makeManager(); |
| 151 | + await manager.start(); |
| 152 | + const pidfile = path.join(manager.path, 'mongocryptd.pid'); |
| 153 | + expect(JSON.parse(await fs.readFile(pidfile, 'utf8')).pid).to.be.a('number'); |
| 154 | + manager.close(); |
| 155 | + |
| 156 | + // The file remains after close, but is gone after creating a new one: |
| 157 | + await fs.stat(pidfile); |
| 158 | + await makeManager().start(); |
| 159 | + try { |
| 160 | + await fs.stat(pidfile); |
| 161 | + expect.fail('missed exception'); |
| 162 | + } catch (e) { |
| 163 | + expect(e.code).to.equal('ENOENT'); |
| 164 | + } |
| 165 | + }); |
| 166 | + |
| 167 | + context('with network testing', () => { |
| 168 | + const testServer = startTestServer('shared'); |
| 169 | + |
| 170 | + beforeEach(async() => { |
| 171 | + process.env.MONGOSH_TEST_PROXY_TARGET_PORT = await testServer.port(); |
| 172 | + }); |
| 173 | + afterEach(() => { |
| 174 | + delete process.env.MONGOSH_TEST_PROXY_TARGET_PORT; |
| 175 | + }); |
| 176 | + |
| 177 | + it('performs keepalive pings', async() => { |
| 178 | + spawnPaths = [[process.execPath, path.resolve(fakeMongocryptdDir, 'withnetworking.js')]]; |
| 179 | + const manager = makeManager(); |
| 180 | + manager.idleShutdownTimeoutSecs = 1; |
| 181 | + await manager.start(); |
| 182 | + const pidfile = path.join(manager.path, 'mongocryptd.pid'); |
| 183 | + await promisify(setTimeout)(2000); |
| 184 | + expect(JSON.parse(await fs.readFile(pidfile, 'utf8')).connections).to.be.greaterThan(1); |
| 185 | + }); |
| 186 | + }); |
| 187 | +}); |
0 commit comments