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(() => {
// Clear Dockerode actions recorded for each test
mockedDockerode.resetHistory();
appMock.unmockAll();
});
before(async () => {
@ -120,7 +121,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
healthCheckStubs.forEach((hc) => hc.restore());
// Remove any test data generated
await mockedAPI.cleanUp();
appMock.unmockAll();
targetStateCacheMock.restore();
loggerStub.restore();
});
@ -213,7 +213,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('GET /v1/apps/:appId', () => {
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
.get('/v1/apps/2')
.set('Accept', 'application/json')
@ -236,32 +235,39 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
});
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
// Make request
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
// Make request
await request
.get('/v1/apps/2')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(sampleResponses.V1.GET['/apps/2'].statusCode)
.expect('Content-Type', /json/)
.then((response) => {
expect(response.body).to.deep.equal(
sampleResponses.V1.GET['/apps/2'].body,
);
});
},
);
});
});
describe('POST /v1/apps/:appId/stop', () => {
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
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
.statusCode,
);
await mockedDockerode.testWithData({ containers, images }, async () => {
await request
.post('/v1/apps/2/stop')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(
sampleResponses.V1.GET['/apps/2/stop [Multiple containers running]']
.statusCode,
);
});
});
it('stops a SPECIFIC application and returns a containerId', async () => {
@ -298,11 +304,13 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
describe('POST /v1/apps/:appId/start', () => {
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
await request
.post('/v1/apps/2/start')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(400);
await mockedDockerode.testWithData({ containers, images }, async () => {
await request
.post('/v1/apps/2/start')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(400);
});
});
it('starts a SPECIFIC application and returns a containerId', async () => {
@ -387,14 +395,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.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(rebootMock).to.have.been.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
},
);
});
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.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/reboot')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(rebootMock).to.not.have.been.called;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(rebootMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -450,16 +468,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/reboot')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/reboot')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(rebootMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -488,14 +511,19 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.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(shutdownMock).to.have.been.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
},
);
shutdownMock.resetHistory();
});
@ -520,15 +548,20 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/shutdown')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(423);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(shutdownMock).to.not.have.been.called;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Error').that.is.not.empty;
expect(shutdownMock).to.not.have.been.called;
},
);
(updateLock.lock as SinonStub).restore();
});
@ -553,16 +586,21 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
appMock.mockManagers([container], [], []);
appMock.mockImages([], false, [image]);
const response = await request
.post('/v1/shutdown')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
await mockedDockerode.testWithData(
{ containers: [container], images: [image] },
async () => {
const response = await request
.post('/v1/shutdown')
.send({ force: true })
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${apiKeys.cloudApiKey}`)
.expect(202);
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
expect(updateLock.lock).to.be.calledOnce;
expect(response.body).to.have.property('Data').that.is.not.empty;
expect(shutdownMock).to.have.been.calledOnce;
},
);
(updateLock.lock as SinonStub).restore();
});

View File

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

View File

@ -81,6 +81,12 @@ export function registerOverride<
overrides[name] = fn;
}
export function restoreOverride<T extends DockerodeFunction>(name: T) {
if (overrides.hasOwnProperty(name)) {
delete overrides[name];
}
}
export interface TestData {
networks: Dictionary<any>;
images: Dictionary<any>;
@ -201,6 +207,24 @@ function createMockedDockerode(data: TestData) {
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(
data: Partial<TestData>,
test: () => Promise<any>,
@ -216,7 +240,7 @@ export async function testWithData(
};
// grab the original prototype...
const basePrototype = dockerode.prototype;
const basePrototype = clonePrototype(dockerode.prototype);
// @ts-expect-error setting a RO property
dockerode.prototype = createMockedDockerode(mockedData);
@ -226,7 +250,6 @@ export async function testWithData(
await test();
} finally {
// reset the original prototype...
// @ts-expect-error setting a RO property
dockerode.prototype = basePrototype;
assignPrototype(dockerode.prototype, basePrototype);
}
}