mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-02-07 20:00:25 +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 Bluebird = require('bluebird');
|
||||||
import constants = require('../lib/constants');
|
|
||||||
import log from '../lib/supervisor-console';
|
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import { InternalInconsistencyError } from '../lib/errors';
|
import * as url from 'url';
|
||||||
import { DeviceStatus } from '../types/state';
|
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 { getRequestInstance } from '../lib/request';
|
||||||
|
import * as sysInfo from '../lib/system-info';
|
||||||
|
|
||||||
|
import { DeviceStatus } from '../types/state';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
|
import { SchemaTypeKey, SchemaReturn } from '../config/schema-type';
|
||||||
import * as eventTracker from '../event-tracker';
|
import * as eventTracker from '../event-tracker';
|
||||||
import * as deviceState from '../device-state';
|
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
|
// The exponential backoff starts at 15s
|
||||||
const MINIMUM_BACKOFF_DELAY = 15000;
|
const MINIMUM_BACKOFF_DELAY = 15000;
|
||||||
@ -33,21 +35,23 @@ const stateForReport: DeviceStatus = {
|
|||||||
};
|
};
|
||||||
let reportPending = false;
|
let reportPending = false;
|
||||||
|
|
||||||
class StatusError extends Error {
|
type CurrentStateReportConf = {
|
||||||
constructor(public statusCode: number, public statusMessage?: string) {
|
[key in keyof Pick<
|
||||||
super(statusMessage);
|
config.ConfigMap<SchemaTypeKey>,
|
||||||
}
|
| 'uuid'
|
||||||
}
|
| 'apiEndpoint'
|
||||||
|
| 'apiTimeout'
|
||||||
|
| 'deviceApiKey'
|
||||||
|
| 'deviceId'
|
||||||
|
| 'localMode'
|
||||||
|
>]: SchemaReturn<key>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an object that contains only status fields relevant for the local mode.
|
* Returns an object that contains only status fields relevant for the local mode.
|
||||||
* It basically removes information about applications state.
|
* It basically removes information about applications state.
|
||||||
*
|
|
||||||
* Exported for tests
|
|
||||||
*/
|
*/
|
||||||
export const stripDeviceStateInLocalMode = (
|
const stripDeviceStateInLocalMode = (state: DeviceStatus): DeviceStatus => {
|
||||||
state: DeviceStatus,
|
|
||||||
): DeviceStatus => {
|
|
||||||
return {
|
return {
|
||||||
local: _.cloneDeep(
|
local: _.cloneDeep(
|
||||||
_.omit(state.local, 'apps', 'is_on__commit', 'logs_channel'),
|
_.omit(state.local, 'apps', 'is_on__commit', 'logs_channel'),
|
||||||
@ -57,10 +61,11 @@ export const stripDeviceStateInLocalMode = (
|
|||||||
|
|
||||||
const sendReportPatch = async (
|
const sendReportPatch = async (
|
||||||
stateDiff: DeviceStatus,
|
stateDiff: DeviceStatus,
|
||||||
conf: { apiEndpoint: string; uuid: string; localMode: boolean },
|
conf: Omit<CurrentStateReportConf, 'deviceId'>,
|
||||||
) => {
|
) => {
|
||||||
let body = stateDiff;
|
let body = stateDiff;
|
||||||
if (conf.localMode) {
|
const { apiEndpoint, apiTimeout, deviceApiKey, localMode, uuid } = conf;
|
||||||
|
if (localMode) {
|
||||||
body = stripDeviceStateInLocalMode(stateDiff);
|
body = stripDeviceStateInLocalMode(stateDiff);
|
||||||
// In local mode, check if it still makes sense to send any updates after data strip.
|
// In local mode, check if it still makes sense to send any updates after data strip.
|
||||||
if (_.isEmpty(body.local)) {
|
if (_.isEmpty(body.local)) {
|
||||||
@ -68,12 +73,6 @@ const sendReportPatch = async (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const { uuid, apiEndpoint, apiTimeout, deviceApiKey } = await config.getMany([
|
|
||||||
'uuid',
|
|
||||||
'apiEndpoint',
|
|
||||||
'apiTimeout',
|
|
||||||
'deviceApiKey',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const endpoint = url.resolve(apiEndpoint, `/device/v2/${uuid}/state`);
|
const endpoint = url.resolve(apiEndpoint, `/device/v2/${uuid}/state`);
|
||||||
const request = await getRequestInstance();
|
const request = await getRequestInstance();
|
||||||
@ -101,7 +100,7 @@ const getStateDiff = (): DeviceStatus => {
|
|||||||
const lastReportedDependent = lastReportedState.dependent;
|
const lastReportedDependent = lastReportedState.dependent;
|
||||||
if (lastReportedLocal == null || lastReportedDependent == null) {
|
if (lastReportedLocal == null || lastReportedDependent == null) {
|
||||||
throw new InternalInconsistencyError(
|
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,
|
lastReportedState,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
@ -132,7 +131,7 @@ const getStateDiff = (): DeviceStatus => {
|
|||||||
|
|
||||||
const report = _.throttle(async () => {
|
const report = _.throttle(async () => {
|
||||||
const conf = await config.getMany([
|
const conf = await config.getMany([
|
||||||
'deviceId',
|
'deviceApiKey',
|
||||||
'apiTimeout',
|
'apiTimeout',
|
||||||
'apiEndpoint',
|
'apiEndpoint',
|
||||||
'uuid',
|
'uuid',
|
||||||
@ -144,15 +143,14 @@ const report = _.throttle(async () => {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { apiEndpoint, uuid, localMode } = conf;
|
if (conf.uuid == null || conf.apiEndpoint == null) {
|
||||||
if (uuid == null || apiEndpoint == null) {
|
|
||||||
throw new InternalInconsistencyError(
|
throw new InternalInconsistencyError(
|
||||||
'No uuid or apiEndpoint provided to ApiBinder.report',
|
'No uuid or apiEndpoint provided to CurrentState.report',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendReportPatch(stateDiff, { apiEndpoint, uuid, localMode });
|
await sendReportPatch(stateDiff, conf);
|
||||||
|
|
||||||
stateReportErrors = 0;
|
stateReportErrors = 0;
|
||||||
_.assign(lastReportedState.local, stateDiff.local);
|
_.assign(lastReportedState.local, stateDiff.local);
|
||||||
|
@ -9,6 +9,12 @@ export interface StatusCodeError extends Error {
|
|||||||
statusCode?: string | number;
|
statusCode?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class StatusError extends Error {
|
||||||
|
constructor(public statusCode: number, public statusMessage?: string) {
|
||||||
|
super(statusMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface CodedSysError extends Error {
|
interface CodedSysError extends Error {
|
||||||
code?: string;
|
code?: string;
|
||||||
}
|
}
|
||||||
|
@ -298,8 +298,6 @@ describe('deviceState', () => {
|
|||||||
deviceState.reportCurrentState({ someStateDiff: 'someValue' } as any);
|
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 () => {
|
it.skip('writes the target state to the db with some extra defaults', async () => {
|
||||||
const testTarget = _.cloneDeep(testTargetWithDefaults2);
|
const testTarget = _.cloneDeep(testTargetWithDefaults2);
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ import balenaAPI = require('./lib/mocked-balena-api');
|
|||||||
import { schema } from '../src/config/schema';
|
import { schema } from '../src/config/schema';
|
||||||
import ConfigJsonConfigBackend from '../src/config/configJson';
|
import ConfigJsonConfigBackend from '../src/config/configJson';
|
||||||
import * as TargetState from '../src/device-state/target-state';
|
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 * as ApiHelper from '../src/lib/api-helper';
|
||||||
import supervisorVersion = require('../src/lib/supervisor-version');
|
import supervisorVersion = require('../src/lib/supervisor-version');
|
||||||
import * as eventTracker from '../src/event-tracker';
|
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', () => {
|
describe('healthchecks', () => {
|
||||||
const components: Dictionary<any> = {};
|
const components: Dictionary<any> = {};
|
||||||
let configStub: SinonStub;
|
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