Fix mock dockerode module

Tests using the mock `testWithData` method were not restoring the dockerode
prototype to the default values, making some tests behave differently
when run individually that when run with the suite.

This commit fixes the `testWithData` method and some malfunctioning v1
API tests because of the change. It doesn't fix all the tests
This commit is contained in:
Felipe Lalanne 2021-02-12 18:16:37 -03:00
parent e2d54e9d6c
commit c70aedf044
3 changed files with 157 additions and 90 deletions

View File

@ -73,6 +73,7 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
afterEach(() => { afterEach(() => {
// Clear Dockerode actions recorded for each test // Clear Dockerode actions recorded for each test
mockedDockerode.resetHistory(); mockedDockerode.resetHistory();
appMock.unmockAll();
}); });
before(async () => { before(async () => {
@ -120,7 +121,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
healthCheckStubs.forEach((hc) => hc.restore()); healthCheckStubs.forEach((hc) => hc.restore());
// Remove any test data generated // Remove any test data generated
await mockedAPI.cleanUp(); await mockedAPI.cleanUp();
appMock.unmockAll();
targetStateCacheMock.restore(); targetStateCacheMock.restore();
loggerStub.restore(); loggerStub.restore();
}); });
@ -213,7 +213,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('GET /v1/apps/:appId', () => { describe('GET /v1/apps/:appId', () => {
it('does not return information for an application when there is more than 1 container', async () => { it('does not return information for an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request
await request await request
.get('/v1/apps/2') .get('/v1/apps/2')
.set('Accept', 'application/json') .set('Accept', 'application/json')
@ -236,32 +235,39 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
}); });
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
// Make request await mockedDockerode.testWithData(
await request { containers: [container], images: [image] },
.get('/v1/apps/2') async () => {
.set('Accept', 'application/json') // Make request
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) await request
.expect(sampleResponses.V1.GET['/apps/2'].statusCode) .get('/v1/apps/2')
.expect('Content-Type', /json/) .set('Accept', 'application/json')
.then((response) => { .set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
expect(response.body).to.deep.equal( .expect(sampleResponses.V1.GET['/apps/2'].statusCode)
sampleResponses.V1.GET['/apps/2'].body, .expect('Content-Type', /json/)
); .then((response) => {
}); expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
},
);
}); });
}); });
describe('POST /v1/apps/:appId/stop', () => { describe('POST /v1/apps/:appId/stop', () => {
it('does not allow stopping an application when there is more than 1 container', async () => { it('does not allow stopping an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request // Every test case in this suite has a 3 service release mocked so just make the request
await request await mockedDockerode.testWithData({ containers, images }, async () => {
.post('/v1/apps/2/stop') await request
.set('Accept', 'application/json') .post('/v1/apps/2/stop')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) .set('Accept', 'application/json')
.expect( .set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]'] .expect(
.statusCode, sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
); .statusCode,
);
});
}); });
it('stops a SPECIFIC application and returns a containerId', async () => { it('stops a SPECIFIC application and returns a containerId', async () => {
@ -298,11 +304,13 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('POST /v1/apps/:appId/start', () => { describe('POST /v1/apps/:appId/start', () => {
it('does not allow starting an application when there is more than 1 container', async () => { it('does not allow starting an application when there is more than 1 container', async () => {
// Every test case in this suite has a 3 service release mocked so just make the request // Every test case in this suite has a 3 service release mocked so just make the request
await request await mockedDockerode.testWithData({ containers, images }, async () => {
.post('/v1/apps/2/start') await request
.set('Accept', 'application/json') .post('/v1/apps/2/start')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) .set('Accept', 'application/json')
.expect(400); .set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(400);
});
}); });
it('starts a SPECIFIC application and returns a containerId', async () => { it('starts a SPECIFIC application and returns a containerId', async () => {
@ -387,14 +395,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/reboot') { containers: [container], images: [image] },
.set('Accept', 'application/json') async () => {
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) const response = await request
.expect(202); .post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(response.body).to.have.property('Data').that.is.not.empty; expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce; expect(rebootMock).to.have.been.calledOnce;
},
);
}); });
it('should return 423 and reject the reboot if no locks are set', async () => { it('should return 423 and reject the reboot if no locks are set', async () => {
@ -417,15 +430,20 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/reboot') { containers: [container], images: [image] },
.set('Accept', 'application/json') async () => {
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) const response = await request
.expect(423); .post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce; expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty; expect(response.body).to.have.property('Error').that.is.not.empty;
expect(rebootMock).to.not.have.been.called; expect(rebootMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore(); (updateLock.lock as SinonStub).restore();
}); });
@ -450,16 +468,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/reboot') { containers: [container], images: [image] },
.send({ force: true }) async () => {
.set('Accept', 'application/json') const response = await request
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) .post('/v1/reboot')
.expect(202); .send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce; expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty; expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce; expect(rebootMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore(); (updateLock.lock as SinonStub).restore();
}); });
@ -488,14 +511,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/shutdown') { containers: [container], images: [image] },
.set('Accept', 'application/json') async () => {
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) const response = await request
.expect(202); .post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(response.body).to.have.property('Data').that.is.not.empty; expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce; expect(shutdownMock).to.have.been.calledOnce;
},
);
shutdownMock.resetHistory(); shutdownMock.resetHistory();
}); });
@ -520,15 +548,20 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/shutdown') { containers: [container], images: [image] },
.set('Accept', 'application/json') async () => {
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) const response = await request
.expect(423); .post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce; expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty; expect(response.body).to.have.property('Error').that.is.not.empty;
expect(shutdownMock).to.not.have.been.called; expect(shutdownMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore(); (updateLock.lock as SinonStub).restore();
}); });
@ -553,16 +586,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []); appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]); appMock.mockImages([], false, [image]);
const response = await request await mockedDockerode.testWithData(
.post('/v1/shutdown') { containers: [container], images: [image] },
.send({ force: true }) async () => {
.set('Accept', 'application/json') const response = await request
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`) .post('/v1/shutdown')
.expect(202); .send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce; expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty; expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce; expect(shutdownMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore(); (updateLock.lock as SinonStub).restore();
}); });

View File

@ -361,13 +361,15 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
}); });
it('should return 404 for an unknown service', async () => { it('should return 404 for an unknown service', async () => {
await request await mockedDockerode.testWithData({}, async () => {
.post(`/v2/applications/1658654/start-service?apikey=${appScopedKey}`) await request
.send({ serviceName: 'unknown' }) .post(`/v2/applications/1658654/start-service?apikey=${appScopedKey}`)
.set('Content-type', 'application/json') .send({ serviceName: 'unknown' })
.expect(404); .set('Content-type', 'application/json')
.expect(404);
expect(applicationManagerSpy).to.not.have.been.called; expect(applicationManagerSpy).to.not.have.been.called;
});
}); });
it('should ignore locks and return 200', async () => { it('should ignore locks and return 200', async () => {
@ -465,12 +467,16 @@ describe('SupervisorAPI [V2 Endpoints]', () => {
}); });
it('should return 404 for an unknown service', async () => { it('should return 404 for an unknown service', async () => {
await request await mockedDockerode.testWithData({}, async () => {
.post(`/v2/applications/1658654/restart-service?apikey=${appScopedKey}`) await request
.send({ serviceName: 'unknown' }) .post(
.set('Content-type', 'application/json') `/v2/applications/1658654/restart-service?apikey=${appScopedKey}`,
.expect(404); )
expect(applicationManagerSpy).to.not.have.been.called; .send({ serviceName: 'unknown' })
.set('Content-type', 'application/json')
.expect(404);
expect(applicationManagerSpy).to.not.have.been.called;
});
}); });
it('should return 423 for a service with update locks', async () => { it('should return 423 for a service with update locks', async () => {

View File

@ -81,6 +81,12 @@ export function registerOverride<
overrides[name] = fn; overrides[name] = fn;
} }
export function restoreOverride<T extends DockerodeFunction>(name: T) {
if (overrides.hasOwnProperty(name)) {
delete overrides[name];
}
}
export interface TestData { export interface TestData {
networks: Dictionary<any>; networks: Dictionary<any>;
images: Dictionary<any>; images: Dictionary<any>;
@ -201,6 +207,24 @@ function createMockedDockerode(data: TestData) {
return mockedDockerode; return mockedDockerode;
} }
type Prototype = Dictionary<(...args: any[]) => any>;
function clonePrototype(prototype: Prototype): Prototype {
const clone: Prototype = {};
Object.getOwnPropertyNames(prototype).forEach((fn) => {
if (fn !== 'constructor' && _.isFunction(prototype[fn])) {
clone[fn] = prototype[fn];
}
});
return clone;
}
function assignPrototype(target: Prototype, source: Prototype) {
Object.keys(source).forEach((fn) => {
target[fn] = source[fn];
});
}
export async function testWithData( export async function testWithData(
data: Partial<TestData>, data: Partial<TestData>,
test: () => Promise<any>, test: () => Promise<any>,
@ -216,7 +240,7 @@ export async function testWithData(
}; };
// grab the original prototype... // grab the original prototype...
const basePrototype = dockerode.prototype; const basePrototype = clonePrototype(dockerode.prototype);
// @ts-expect-error setting a RO property // @ts-expect-error setting a RO property
dockerode.prototype = createMockedDockerode(mockedData); dockerode.prototype = createMockedDockerode(mockedData);
@ -226,7 +250,6 @@ export async function testWithData(
await test(); await test();
} finally { } finally {
// reset the original prototype... // reset the original prototype...
// @ts-expect-error setting a RO property assignPrototype(dockerode.prototype, basePrototype);
dockerode.prototype = basePrototype;
} }
} }