balena-supervisor/test/40-target-state.spec.ts
Felipe Lalanne e4e895630f Ensure the first target state request is applied
During first time run of the supervisor, the target state is queried
by `reportInitialEnv`. Since this happens early on the initialization
process, this target state report is missed by any listeners and this
can lead to the initial target state not beeing applied (see #1455).

This PR ensures that target state is re-emitted if there were no
listeners setup on call to update.

Change-type: patch
Signed-off-by: Felipe Lalanne <felipe@balena.io>
Connects-to: #1455
2020-11-13 10:19:27 -03:00

236 lines
5.8 KiB
TypeScript

import { SinonStub, stub, spy, SinonSpy } from 'sinon';
import { Promise } from 'bluebird';
import * as _ from 'lodash';
import { expect } from './lib/chai-config';
import * as TargetState from '../src/device-state/target-state';
import * as request from '../src/lib/request';
const stateEndpointBody = {
local: {
name: 'solitary-bush',
config: {},
apps: {
'123': {
name: 'my-app',
services: {},
volumes: {},
networks: {},
},
},
},
dependent: {
apps: {},
devices: {},
},
};
describe('Target state', () => {
describe('update', () => {
it('should emit target state when a new one is available', async () => {
const req = {
getAsync: () =>
Promise.resolve([
{
statusCode: 200,
headers: {},
} as any,
JSON.stringify(stateEndpointBody),
]),
};
spy(req, 'getAsync');
stub(request, 'getRequestInstance').resolves(req as any);
// Setup target state listener
const listener = stub();
TargetState.emitter.on('target-state-update', listener);
// Perform target state request
await TargetState.update();
expect(request.getRequestInstance).to.be.calledOnce;
// The getAsync method gets called once and includes authorization headers
expect(req.getAsync).to.be.calledOnce;
expect(
_.has((req.getAsync as SinonSpy).args[0], ['headers', 'Authorization']),
);
// The listener should receive the endpoint
expect(listener).to.be.calledOnceWith(JSON.stringify(stateEndpointBody));
// Remove getRequestInstance stub
(request.getRequestInstance as SinonStub).restore();
// new request returns 304
const newReq = {
getAsync: () =>
Promise.resolve([
{
statusCode: 304,
headers: {},
} as any,
]),
};
spy(newReq, 'getAsync');
stub(request, 'getRequestInstance').resolves(newReq as any);
// Perform new target state request
await TargetState.update();
// The new req should have been called
expect(newReq.getAsync).to.be.calledOnce;
// No new calls to the listener
expect(listener).to.be.calledOnce;
// Cleanup
TargetState.emitter.off('target-state-update', listener);
(request.getRequestInstance as SinonStub).restore();
});
it('should emit cached target state if there was no listener for the cached state', async () => {
const req = {
getAsync: () =>
Promise.resolve([
{
statusCode: 200,
headers: {},
} as any,
JSON.stringify(stateEndpointBody),
]),
};
spy(req, 'getAsync');
stub(request, 'getRequestInstance').resolves(req as any);
// Perform target state request
await TargetState.update();
expect(request.getRequestInstance).to.be.calledOnce;
// The getAsync method gets called once and includes authorization headers
expect(req.getAsync).to.be.calledOnce;
expect(
_.has((req.getAsync as SinonSpy).args[0], ['headers', 'Authorization']),
);
// Remove getRequestInstance stub
(request.getRequestInstance as SinonStub).restore();
// new request returns 304
const newReq = {
getAsync: () =>
Promise.resolve([
{
statusCode: 304,
headers: {},
} as any,
]),
};
spy(newReq, 'getAsync');
stub(request, 'getRequestInstance').resolves(newReq as any);
// Setup target state listener after the first request
const listener = stub();
TargetState.emitter.on('target-state-update', listener);
// Perform new target state request
await TargetState.update();
// The new req should have been called
expect(newReq.getAsync).to.be.calledOnce;
// The listener should receive the endpoint
expect(listener).to.be.calledOnceWith(JSON.stringify(stateEndpointBody));
// Cleanup
TargetState.emitter.off('target-state-update', listener);
(request.getRequestInstance as SinonStub).restore();
});
});
describe('get', () => {
it('returns the latest target state endpoint response', async () => {
const req = {
getAsync: () =>
Promise.resolve([
{
statusCode: 200,
headers: {},
} as any,
JSON.stringify(stateEndpointBody),
]),
};
// Setup spies and stubs
spy(req, 'getAsync');
stub(request, 'getRequestInstance').resolves(req as any);
// Perform target state request
const response = await TargetState.get();
// The stubbed methods should only be called once
expect(request.getRequestInstance).to.be.calledOnce;
expect(req.getAsync).to.be.calledOnce;
// Cached value should reflect latest response
expect(response).to.be.equal(JSON.stringify(stateEndpointBody));
// Cleanup
(request.getRequestInstance as SinonStub).restore();
});
it('returns the last cached target state', async () => {
const req = {
getAsync: () =>
Promise.resolve([
{
statusCode: 200,
headers: {},
} as any,
JSON.stringify(stateEndpointBody),
]),
};
// Setup spies and stubs
stub(request, 'getRequestInstance').resolves(req as any);
// Perform target state request, this should
// put the query result in the cache
await TargetState.update();
// Reset the stub
(request.getRequestInstance as SinonStub).restore();
const newReq = {
getAsync: () =>
Promise.resolve([
{
statusCode: 304,
headers: {},
} as any,
]),
};
spy(newReq, 'getAsync');
stub(request, 'getRequestInstance').resolves(newReq as any);
// Perform target state request
const response = await TargetState.get();
// The stubbed methods should only be called once
expect(request.getRequestInstance).to.be.calledOnce;
expect(newReq.getAsync).to.be.calledOnce;
// Cached value should reflect latest response
expect(response).to.be.equal(JSON.stringify(stateEndpointBody));
// Cleanup
(request.getRequestInstance as SinonStub).restore();
});
});
});