mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-31 16:35:23 +00:00
Create & unify src/device-state/current-state tests
Signed-off-by: Christina Wang <christina@balena.io>
This commit is contained in:
parent
39601473c0
commit
ea3e50e96e
@ -1,17 +1,19 @@
|
||||
import Bluebird = require('bluebird');
|
||||
import constants = require('../lib/constants');
|
||||
import log from '../lib/supervisor-console';
|
||||
import * as _ from 'lodash';
|
||||
import { InternalInconsistencyError } from '../lib/errors';
|
||||
import { DeviceStatus } from '../types/state';
|
||||
import * as url from 'url';
|
||||
import { CoreOptions } from 'request';
|
||||
|
||||
import * as constants from '../lib/constants';
|
||||
import { log } from '../lib/supervisor-console';
|
||||
import { InternalInconsistencyError, StatusError } from '../lib/errors';
|
||||
import { getRequestInstance } from '../lib/request';
|
||||
import * as sysInfo from '../lib/system-info';
|
||||
|
||||
import { DeviceStatus } from '../types/state';
|
||||
import * as config from '../config';
|
||||
import { SchemaTypeKey, SchemaReturn } from '../config/schema-type';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
import * as deviceState from '../device-state';
|
||||
import { CoreOptions } from 'request';
|
||||
import * as url from 'url';
|
||||
|
||||
import * as sysInfo from '../lib/system-info';
|
||||
|
||||
// The exponential backoff starts at 15s
|
||||
const MINIMUM_BACKOFF_DELAY = 15000;
|
||||
@ -33,21 +35,23 @@ const stateForReport: DeviceStatus = {
|
||||
};
|
||||
let reportPending = false;
|
||||
|
||||
class StatusError extends Error {
|
||||
constructor(public statusCode: number, public statusMessage?: string) {
|
||||
super(statusMessage);
|
||||
}
|
||||
}
|
||||
type CurrentStateReportConf = {
|
||||
[key in keyof Pick<
|
||||
config.ConfigMap<SchemaTypeKey>,
|
||||
| 'uuid'
|
||||
| 'apiEndpoint'
|
||||
| 'apiTimeout'
|
||||
| 'deviceApiKey'
|
||||
| 'deviceId'
|
||||
| 'localMode'
|
||||
>]: SchemaReturn<key>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an object that contains only status fields relevant for the local mode.
|
||||
* It basically removes information about applications state.
|
||||
*
|
||||
* Exported for tests
|
||||
*/
|
||||
export const stripDeviceStateInLocalMode = (
|
||||
state: DeviceStatus,
|
||||
): DeviceStatus => {
|
||||
const stripDeviceStateInLocalMode = (state: DeviceStatus): DeviceStatus => {
|
||||
return {
|
||||
local: _.cloneDeep(
|
||||
_.omit(state.local, 'apps', 'is_on__commit', 'logs_channel'),
|
||||
@ -57,10 +61,11 @@ export const stripDeviceStateInLocalMode = (
|
||||
|
||||
const sendReportPatch = async (
|
||||
stateDiff: DeviceStatus,
|
||||
conf: { apiEndpoint: string; uuid: string; localMode: boolean },
|
||||
conf: Omit<CurrentStateReportConf, 'deviceId'>,
|
||||
) => {
|
||||
let body = stateDiff;
|
||||
if (conf.localMode) {
|
||||
const { apiEndpoint, apiTimeout, deviceApiKey, localMode, uuid } = conf;
|
||||
if (localMode) {
|
||||
body = stripDeviceStateInLocalMode(stateDiff);
|
||||
// In local mode, check if it still makes sense to send any updates after data strip.
|
||||
if (_.isEmpty(body.local)) {
|
||||
@ -68,12 +73,6 @@ const sendReportPatch = async (
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { uuid, apiEndpoint, apiTimeout, deviceApiKey } = await config.getMany([
|
||||
'uuid',
|
||||
'apiEndpoint',
|
||||
'apiTimeout',
|
||||
'deviceApiKey',
|
||||
]);
|
||||
|
||||
const endpoint = url.resolve(apiEndpoint, `/device/v2/${uuid}/state`);
|
||||
const request = await getRequestInstance();
|
||||
@ -101,7 +100,7 @@ const getStateDiff = (): DeviceStatus => {
|
||||
const lastReportedDependent = lastReportedState.dependent;
|
||||
if (lastReportedLocal == null || lastReportedDependent == null) {
|
||||
throw new InternalInconsistencyError(
|
||||
`No local or dependent component of lastReportedLocal in ApiBinder.getStateDiff: ${JSON.stringify(
|
||||
`No local or dependent component of lastReportedLocal in CurrentState.getStateDiff: ${JSON.stringify(
|
||||
lastReportedState,
|
||||
)}`,
|
||||
);
|
||||
@ -132,7 +131,7 @@ const getStateDiff = (): DeviceStatus => {
|
||||
|
||||
const report = _.throttle(async () => {
|
||||
const conf = await config.getMany([
|
||||
'deviceId',
|
||||
'deviceApiKey',
|
||||
'apiTimeout',
|
||||
'apiEndpoint',
|
||||
'uuid',
|
||||
@ -144,15 +143,14 @@ const report = _.throttle(async () => {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { apiEndpoint, uuid, localMode } = conf;
|
||||
if (uuid == null || apiEndpoint == null) {
|
||||
if (conf.uuid == null || conf.apiEndpoint == null) {
|
||||
throw new InternalInconsistencyError(
|
||||
'No uuid or apiEndpoint provided to ApiBinder.report',
|
||||
'No uuid or apiEndpoint provided to CurrentState.report',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await sendReportPatch(stateDiff, { apiEndpoint, uuid, localMode });
|
||||
await sendReportPatch(stateDiff, conf);
|
||||
|
||||
stateReportErrors = 0;
|
||||
_.assign(lastReportedState.local, stateDiff.local);
|
||||
|
@ -9,6 +9,12 @@ export interface StatusCodeError extends Error {
|
||||
statusCode?: string | number;
|
||||
}
|
||||
|
||||
export class StatusError extends Error {
|
||||
constructor(public statusCode: number, public statusMessage?: string) {
|
||||
super(statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
interface CodedSysError extends Error {
|
||||
code?: string;
|
||||
}
|
||||
|
@ -298,8 +298,6 @@ describe('deviceState', () => {
|
||||
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
||||
});
|
||||
|
||||
it('returns the current state');
|
||||
|
||||
it.skip('writes the target state to the db with some extra defaults', async () => {
|
||||
const testTarget = _.cloneDeep(testTargetWithDefaults2);
|
||||
|
||||
|
@ -12,8 +12,6 @@ import balenaAPI = require('./lib/mocked-balena-api');
|
||||
import { schema } from '../src/config/schema';
|
||||
import ConfigJsonConfigBackend from '../src/config/configJson';
|
||||
import * as TargetState from '../src/device-state/target-state';
|
||||
import { DeviceStatus } from '../src/types/state';
|
||||
import * as CurrentState from '../src/device-state/current-state';
|
||||
import * as ApiHelper from '../src/lib/api-helper';
|
||||
import supervisorVersion = require('../src/lib/supervisor-version');
|
||||
import * as eventTracker from '../src/event-tracker';
|
||||
@ -319,51 +317,6 @@ describe('ApiBinder', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('local mode', () => {
|
||||
const components: Dictionary<any> = {};
|
||||
|
||||
before(() => {
|
||||
return initModels(components, '/config-apibinder.json');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
// @ts-expect-error setting read-only property
|
||||
config.configJsonBackend = defaultConfigBackend;
|
||||
await config.generateRequiredFields();
|
||||
});
|
||||
|
||||
const sampleState = {
|
||||
local: {
|
||||
ip_address: '192.168.1.42 192.168.1.99',
|
||||
api_port: 48484,
|
||||
api_secret:
|
||||
'20ffbd6e15aba827dca6381912d6aeb6c3a7a7c7206d4dfadf0d2f0a9e1136',
|
||||
os_version: 'balenaOS 2.32.0+rev4',
|
||||
os_variant: 'dev',
|
||||
supervisor_version: '9.16.3',
|
||||
provisioning_progress: null,
|
||||
provisioning_state: '',
|
||||
status: 'Idle',
|
||||
logs_channel: null,
|
||||
apps: {},
|
||||
is_on__commit: 'whatever',
|
||||
},
|
||||
dependent: { apps: {} },
|
||||
} as DeviceStatus;
|
||||
|
||||
it('should strip applications data', () => {
|
||||
const result = CurrentState.stripDeviceStateInLocalMode(
|
||||
sampleState,
|
||||
) as Dictionary<any>;
|
||||
expect(result).to.not.have.property('dependent');
|
||||
|
||||
const local = result['local'];
|
||||
expect(local).to.not.have.property('apps');
|
||||
expect(local).to.not.have.property('is_on__commit');
|
||||
expect(local).to.not.have.property('logs_channel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('healthchecks', () => {
|
||||
const components: Dictionary<any> = {};
|
||||
let configStub: SinonStub;
|
||||
|
473
test/src/device-state/current-state.spec.ts
Normal file
473
test/src/device-state/current-state.spec.ts
Normal file
@ -0,0 +1,473 @@
|
||||
import { expect } from 'chai';
|
||||
import { stub, SinonStub, spy, SinonSpy } from 'sinon';
|
||||
import rewire = require('rewire');
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import * as config from '../../../src/config';
|
||||
import * as deviceState from '../../../src/device-state';
|
||||
import { stateReportErrors } from '../../../src/device-state/current-state';
|
||||
|
||||
import * as request from '../../../src/lib/request';
|
||||
import log from '../../../src/lib/supervisor-console';
|
||||
import {
|
||||
InternalInconsistencyError,
|
||||
StatusError,
|
||||
} from '../../../src/lib/errors';
|
||||
import * as sysInfo from '../../../src/lib/system-info';
|
||||
|
||||
describe('device-state/current-state', () => {
|
||||
// Set up rewire to track private methods and variables
|
||||
const currentState = rewire('../../../src/device-state/current-state');
|
||||
const stripDeviceStateInLocalMode = currentState.__get__(
|
||||
'stripDeviceStateInLocalMode',
|
||||
);
|
||||
const sendReportPatch = currentState.__get__('sendReportPatch');
|
||||
const getStateDiff = currentState.__get__('getStateDiff');
|
||||
const lastReportedState = currentState.__get__('lastReportedState');
|
||||
const stateForReport = currentState.__get__('stateForReport');
|
||||
|
||||
const resetGlobalStateObjects = () => {
|
||||
lastReportedState.local = {};
|
||||
lastReportedState.dependent = {};
|
||||
stateForReport.local = {};
|
||||
stateForReport.dependent = {};
|
||||
};
|
||||
|
||||
// Global spies & stubs
|
||||
let stubbedGetReqInstance: SinonStub;
|
||||
// Mock the patchAsync method from src/lib/request.ts
|
||||
let patchAsyncSpy: SinonSpy;
|
||||
const requestInstance = {
|
||||
patchAsync: () => Bluebird.resolve([{ statusCode: 200 }]),
|
||||
};
|
||||
|
||||
// Suite-level hooks
|
||||
before(() => {
|
||||
stubbedGetReqInstance = stub(request, 'getRequestInstance');
|
||||
stubbedGetReqInstance.resolves(requestInstance);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
stubbedGetReqInstance.restore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetGlobalStateObjects();
|
||||
patchAsyncSpy = spy(requestInstance, 'patchAsync');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
patchAsyncSpy.restore();
|
||||
});
|
||||
|
||||
// Test fixtures
|
||||
const testDeviceConf = {
|
||||
uuid: 'testuuid',
|
||||
apiEndpoint: 'http://127.0.0.1',
|
||||
apiTimeout: 100,
|
||||
deviceApiKey: 'testapikey',
|
||||
deviceId: 1337,
|
||||
localMode: false,
|
||||
disableHardwareMetrics: true,
|
||||
};
|
||||
|
||||
const testDeviceReportFields = {
|
||||
api_port: 48484,
|
||||
api_secret:
|
||||
'20ffbd6e15aba827dca6381912d6aeb6c3a7a7c7206d4dfadf0d2f0a9e1136',
|
||||
ip_address: '192.168.1.42 192.168.1.99',
|
||||
os_version: 'balenaOS 2.32.0+rev4',
|
||||
os_variant: 'dev',
|
||||
supervisor_version: '9.16.3',
|
||||
provisioning_progress: null,
|
||||
provisioning_state: '',
|
||||
status: 'Idle',
|
||||
update_failed: false,
|
||||
update_pending: false,
|
||||
update_downloaded: false,
|
||||
is_on__commit: 'whatever',
|
||||
logs_channel: null,
|
||||
mac_addresss: '1C:69:7A:6E:B2:FE D8:3B:BF:51:F1:E4',
|
||||
};
|
||||
|
||||
const testStateConfig = {
|
||||
ENV_VAR_1: '1',
|
||||
ENV_VAR_2: '1',
|
||||
};
|
||||
|
||||
const testStateApps = {
|
||||
'123': {
|
||||
services: {
|
||||
'456': {
|
||||
status: 'Downloaded',
|
||||
releaseId: '12345',
|
||||
download_progress: null,
|
||||
},
|
||||
'789': {
|
||||
status: 'Downloaded',
|
||||
releaseId: '12346',
|
||||
download_progress: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const testStateApps2 = {
|
||||
'321': {
|
||||
services: {
|
||||
'654': {
|
||||
status: 'Downloaded',
|
||||
releaseId: '12347',
|
||||
download_progress: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const testCurrentState = {
|
||||
local: {
|
||||
...testDeviceReportFields,
|
||||
config: testStateConfig,
|
||||
apps: testStateApps,
|
||||
},
|
||||
dependent: { apps: testStateApps2 },
|
||||
commit: 'whatever',
|
||||
};
|
||||
|
||||
describe('stripDeviceStateInLocalMode', () => {
|
||||
it('should strip applications data', () => {
|
||||
const result = stripDeviceStateInLocalMode(testCurrentState);
|
||||
|
||||
expect(result).to.not.have.property('dependent');
|
||||
|
||||
const local = result['local'];
|
||||
expect(local).to.not.have.property('apps');
|
||||
expect(local).to.not.have.property('is_on__commit');
|
||||
expect(local).to.not.have.property('logs_channel');
|
||||
});
|
||||
|
||||
it('should not mutate the input state', () => {
|
||||
const result = stripDeviceStateInLocalMode(testCurrentState);
|
||||
expect(result).to.not.deep.equal(testCurrentState);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendReportPatch', () => {
|
||||
it('should only strip app state in local mode', async () => {
|
||||
// Strip state in local mode
|
||||
await sendReportPatch(testCurrentState, {
|
||||
...testDeviceConf,
|
||||
localMode: true,
|
||||
});
|
||||
// Request body's stripped state should be different than input state
|
||||
expect(patchAsyncSpy.lastCall.lastArg.body).to.not.deep.equal(
|
||||
testCurrentState,
|
||||
);
|
||||
|
||||
// Don't strip state out of local mode
|
||||
await sendReportPatch(testCurrentState, testDeviceConf);
|
||||
expect(patchAsyncSpy.lastCall.lastArg.body).to.deep.equal(
|
||||
testCurrentState,
|
||||
);
|
||||
});
|
||||
|
||||
it('should patch state with empty local objects depending on local mode config', async () => {
|
||||
// Don't patch state with empty state.local in local mode
|
||||
await sendReportPatch(
|
||||
{ ...testCurrentState, local: {} },
|
||||
{ ...testDeviceConf, localMode: true },
|
||||
);
|
||||
expect(patchAsyncSpy).to.not.have.been.called;
|
||||
|
||||
// Patch state with empty state.local out of local mode
|
||||
await sendReportPatch({ ...testCurrentState, local: {} }, testDeviceConf);
|
||||
expect(patchAsyncSpy).to.have.been.called;
|
||||
});
|
||||
|
||||
it('should patch with specified endpoint and params', async () => {
|
||||
await sendReportPatch(testCurrentState, testDeviceConf);
|
||||
const [endpoint, params] = patchAsyncSpy.lastCall.args;
|
||||
expect(endpoint).to.equal(
|
||||
`${testDeviceConf.apiEndpoint}/device/v2/${testDeviceConf.uuid}/state`,
|
||||
);
|
||||
expect(params).to.deep.equal({
|
||||
json: true,
|
||||
headers: { Authorization: `Bearer ${testDeviceConf.deviceApiKey}` },
|
||||
body: testCurrentState,
|
||||
});
|
||||
});
|
||||
|
||||
it('should timeout patch request after apiTimeout milliseconds', async () => {
|
||||
// Overwrite mock patchAsync to delay past apiTimeout ms
|
||||
patchAsyncSpy.restore();
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.delay(
|
||||
testDeviceConf.apiTimeout + 100,
|
||||
Bluebird.resolve([{ statusCode: 200 }]),
|
||||
);
|
||||
patchAsyncSpy = spy(requestInstance, 'patchAsync');
|
||||
|
||||
await expect(
|
||||
sendReportPatch(testCurrentState, testDeviceConf),
|
||||
).to.be.rejectedWith(Bluebird.TimeoutError);
|
||||
|
||||
// Reset to default patchAsync
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 200 }]);
|
||||
});
|
||||
|
||||
it('should communicate string error messages from the API', async () => {
|
||||
// Overwrite mock patchAsync to reject patch request
|
||||
patchAsyncSpy.restore();
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 400, body: 'string error message' }]);
|
||||
patchAsyncSpy = spy(requestInstance, 'patchAsync');
|
||||
|
||||
stub(log, 'error');
|
||||
|
||||
await expect(
|
||||
sendReportPatch(testCurrentState, testDeviceConf),
|
||||
).to.be.rejected.then((err) => {
|
||||
expect(err).to.be.instanceOf(StatusError);
|
||||
expect(err).to.have.property('statusMessage', '"string error message"');
|
||||
});
|
||||
expect(log.error as SinonStub).to.have.been.calledWith(
|
||||
`Error from the API: ${400}`,
|
||||
);
|
||||
|
||||
// Reset to default patchAsync
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 200 }]);
|
||||
|
||||
(log.error as SinonStub).restore();
|
||||
});
|
||||
|
||||
it('should communicate multiline object error messages from the API', async () => {
|
||||
const objectErrorMessage = {
|
||||
testKey: 'testErrorVal',
|
||||
testChild: { testNestedKey: 'testNestedVal' },
|
||||
};
|
||||
// Overwrite mock patchAsync to reject patch request
|
||||
patchAsyncSpy.restore();
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([
|
||||
{
|
||||
statusCode: 400,
|
||||
body: objectErrorMessage,
|
||||
},
|
||||
]);
|
||||
patchAsyncSpy = spy(requestInstance, 'patchAsync');
|
||||
|
||||
stub(log, 'error');
|
||||
|
||||
await expect(
|
||||
sendReportPatch(testCurrentState, testDeviceConf),
|
||||
).to.be.rejected.then((err) => {
|
||||
expect(err).to.be.instanceOf(StatusError);
|
||||
expect(err).to.have.property(
|
||||
'statusMessage',
|
||||
JSON.stringify(objectErrorMessage, null, 2),
|
||||
);
|
||||
});
|
||||
expect(log.error as SinonStub).to.have.been.calledWith(
|
||||
`Error from the API: ${400}`,
|
||||
);
|
||||
|
||||
// Reset to default patchAsync
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 200 }]);
|
||||
|
||||
(log.error as SinonStub).restore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStateDiff', () => {
|
||||
it('should error if last reported state is missing local or dependent properties', () => {
|
||||
lastReportedState.local = null;
|
||||
lastReportedState.dependent = null;
|
||||
|
||||
expect(() => getStateDiff()).to.throw(InternalInconsistencyError);
|
||||
});
|
||||
|
||||
it('should not modify global stateForReport or lastReportedState after call', async () => {
|
||||
lastReportedState.local = {
|
||||
status: 'Downloading',
|
||||
config: testStateConfig,
|
||||
};
|
||||
stateForReport.local = {
|
||||
status: 'Idle',
|
||||
config: { ...testStateConfig, ENV_VAR_3: '1' },
|
||||
};
|
||||
getStateDiff();
|
||||
expect(lastReportedState.local).to.deep.equal({
|
||||
status: 'Downloading',
|
||||
config: {
|
||||
ENV_VAR_1: '1',
|
||||
ENV_VAR_2: '1',
|
||||
},
|
||||
});
|
||||
expect(stateForReport.local).to.deep.equal({
|
||||
status: 'Idle',
|
||||
config: {
|
||||
ENV_VAR_1: '1',
|
||||
ENV_VAR_2: '1',
|
||||
ENV_VAR_3: '1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return any changed fields', async () => {
|
||||
// No diffs when lastReportedState and stateForReport are the same
|
||||
expect(getStateDiff()).to.deep.equal({});
|
||||
|
||||
// Changed config fields
|
||||
lastReportedState.local = {
|
||||
config: { ENV_VAR_3: '1' },
|
||||
};
|
||||
stateForReport.local = {
|
||||
config: { ENV_VAR_3: '0' },
|
||||
};
|
||||
expect(getStateDiff()).to.deep.equal({
|
||||
local: { config: { ENV_VAR_3: '0' } },
|
||||
});
|
||||
resetGlobalStateObjects();
|
||||
|
||||
// Changed apps fields
|
||||
lastReportedState.local = { apps: testStateApps };
|
||||
stateForReport.local = { apps: testStateApps2 };
|
||||
expect(getStateDiff()).to.deep.equal({ local: { apps: testStateApps2 } });
|
||||
resetGlobalStateObjects();
|
||||
|
||||
// Changed dependent fields
|
||||
lastReportedState.dependent = { apps: testStateApps2 };
|
||||
stateForReport.dependent = { apps: testStateApps };
|
||||
expect(getStateDiff()).to.deep.equal({
|
||||
dependent: { apps: testStateApps },
|
||||
});
|
||||
resetGlobalStateObjects();
|
||||
|
||||
// Changed sys info fields
|
||||
lastReportedState.local = { cpu_temp: 10 };
|
||||
stateForReport.local = { cpu_temp: 16 };
|
||||
expect(getStateDiff()).to.deep.equal({ local: { cpu_temp: 16 } });
|
||||
resetGlobalStateObjects();
|
||||
});
|
||||
|
||||
it('should omit internal state keys and report DeviceReportField (type) info', async () => {
|
||||
// INTERNAL_STATE_KEYS are: ['update_pending', 'update_downloaded', 'update_failed']
|
||||
stateForReport.local = _.pick(testDeviceReportFields, [
|
||||
'update_pending',
|
||||
'update_downloaded',
|
||||
'update_failed',
|
||||
'os_version',
|
||||
]);
|
||||
stateForReport.dependent = _.pick(testDeviceReportFields, [
|
||||
'update_pending',
|
||||
'update_downloaded',
|
||||
'update_failed',
|
||||
'status',
|
||||
]);
|
||||
expect(getStateDiff()).to.deep.equal({
|
||||
local: _.pick(testDeviceReportFields, ['os_version']),
|
||||
dependent: _.pick(testDeviceReportFields, ['status']),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('throttled report', () => {
|
||||
const report = currentState.__get__('report');
|
||||
|
||||
let configGetManyStub: SinonStub;
|
||||
|
||||
before(() => {
|
||||
configGetManyStub = stub(config, 'getMany');
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 200 }]);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
configGetManyStub.restore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
configGetManyStub.resolves(
|
||||
_.omit(testDeviceConf, ['deviceId', 'disableHardwareMetrics']) as any,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clear the throttle time limit between tests
|
||||
report.cancel();
|
||||
});
|
||||
|
||||
it("doesn't report if current state hasn't changed", async () => {
|
||||
// A beforeEach hook resets the global state objects to all be empty, so
|
||||
// by default, report() will return 0 in this test env
|
||||
expect(await report()).to.equal(0);
|
||||
});
|
||||
|
||||
it('errors when provided invalid uuid or apiEndpoint', async () => {
|
||||
configGetManyStub.resolves(
|
||||
_.omit(testDeviceConf, [
|
||||
'deviceId',
|
||||
'disableHardwareMetrics',
|
||||
'uuid',
|
||||
'apiEndpoint',
|
||||
]) as any,
|
||||
);
|
||||
stateForReport.local = { ...testDeviceReportFields };
|
||||
await expect(report()).to.be.rejectedWith(InternalInconsistencyError);
|
||||
});
|
||||
|
||||
it('resets stateReportErrors to 0 on patch success', async () => {
|
||||
spy(_, 'assign');
|
||||
|
||||
stateForReport.local = { ...testDeviceReportFields };
|
||||
await report();
|
||||
|
||||
expect(stateReportErrors).to.equal(0);
|
||||
expect(_.assign as SinonSpy).to.have.been.calledTwice;
|
||||
|
||||
(_.assign as SinonSpy).restore();
|
||||
});
|
||||
|
||||
it('handles errors on state patch failure', async () => {
|
||||
// Overwrite default patchAsync response to return erroring statusCode
|
||||
patchAsyncSpy.restore();
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 400 }]);
|
||||
patchAsyncSpy = spy(requestInstance, 'patchAsync');
|
||||
|
||||
stub(log, 'error');
|
||||
|
||||
stateForReport.local = { ...testDeviceReportFields };
|
||||
await report();
|
||||
|
||||
expect((log.error as SinonStub).lastCall.args[0]).to.equal(
|
||||
'Non-200 response from the API! Status code: 400 - message:',
|
||||
);
|
||||
(log.error as SinonStub).restore();
|
||||
|
||||
// Reset to default patchAsync
|
||||
requestInstance.patchAsync = () =>
|
||||
Bluebird.resolve([{ statusCode: 200 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skip('reportCurrentState', () => {
|
||||
const reportPending = currentState.__get__('reportPending');
|
||||
|
||||
before(() => {
|
||||
stub(deviceState, 'getStatus').resolves(testCurrentState as any);
|
||||
stub(config, 'get').resolves(true);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
(deviceState.getStatus as SinonStub).restore();
|
||||
(config.get as SinonStub).restore();
|
||||
});
|
||||
|
||||
it('does not report if current state has not changed');
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user