mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-19 13:47:54 +00:00
api-binder: Convert to a singleton
Change-type: patch Signed-off-by: Rich Bayliss <rich@balena.io> Signed-off-by: Cameron Diver <cameron@balena.io>
This commit is contained in:
parent
13241bf6e7
commit
d50f7791e1
1314
src/api-binder.ts
1314
src/api-binder.ts
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ import * as updateLock from './lib/update-lock';
|
||||
import * as validation from './lib/validation';
|
||||
import * as network from './network';
|
||||
|
||||
import APIBinder from './api-binder';
|
||||
import * as APIBinder from './api-binder';
|
||||
import { ApplicationManager } from './application-manager';
|
||||
import * as deviceConfig from './device-config';
|
||||
import { ConfigStep } from './device-config';
|
||||
@ -178,7 +178,7 @@ function createDeviceStateRouter(deviceState: DeviceState) {
|
||||
}
|
||||
|
||||
interface DeviceStateConstructOpts {
|
||||
apiBinder: APIBinder;
|
||||
apiBinder: typeof APIBinder;
|
||||
}
|
||||
|
||||
interface DeviceStateEvents {
|
||||
|
213
src/lib/api-helper.ts
Normal file
213
src/lib/api-helper.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { PinejsClientRequest } from 'pinejs-client-request';
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as config from '../config';
|
||||
import * as eventTracker from '../event-tracker';
|
||||
|
||||
import * as request from './request';
|
||||
import * as deviceRegister from './register-device';
|
||||
import {
|
||||
DeviceNotFoundError,
|
||||
ExchangeKeyError,
|
||||
FailedToProvisionDeviceError,
|
||||
InternalInconsistencyError,
|
||||
isHttpConflictError,
|
||||
} from './errors';
|
||||
import log from './supervisor-console';
|
||||
|
||||
export type KeyExchangeOpts = config.ConfigType<'provisioningOptions'>;
|
||||
|
||||
export interface Device {
|
||||
id: number;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export const fetchDevice = async (
|
||||
balenaApi: PinejsClientRequest,
|
||||
uuid: string,
|
||||
apiKey: string,
|
||||
timeout: number,
|
||||
) => {
|
||||
if (balenaApi == null) {
|
||||
throw new InternalInconsistencyError(
|
||||
'fetchDevice called without an initialized API client',
|
||||
);
|
||||
}
|
||||
|
||||
const reqOpts = {
|
||||
resource: 'device',
|
||||
options: {
|
||||
$filter: {
|
||||
uuid,
|
||||
},
|
||||
},
|
||||
passthrough: {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const [device] = (await Bluebird.resolve(balenaApi.get(reqOpts)).timeout(
|
||||
timeout,
|
||||
)) as Device[];
|
||||
|
||||
if (device == null) {
|
||||
throw new DeviceNotFoundError();
|
||||
}
|
||||
|
||||
return device;
|
||||
} catch (e) {
|
||||
throw new DeviceNotFoundError();
|
||||
}
|
||||
};
|
||||
|
||||
export const exchangeKeyAndGetDeviceOrRegenerate = async (
|
||||
balenaApi: PinejsClientRequest,
|
||||
opts: KeyExchangeOpts,
|
||||
): Promise<Device> => {
|
||||
try {
|
||||
const device = await exchangeKeyAndGetDevice(balenaApi, opts);
|
||||
log.debug('Key exchange succeeded');
|
||||
return device;
|
||||
} catch (e) {
|
||||
if (e instanceof ExchangeKeyError) {
|
||||
log.error('Exchanging key failed, re-registering...');
|
||||
await config.regenerateRegistrationFields();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const exchangeKeyAndGetDevice = async (
|
||||
balenaApi: PinejsClientRequest,
|
||||
opts: Partial<KeyExchangeOpts>,
|
||||
): Promise<Device> => {
|
||||
const uuid = opts.uuid;
|
||||
const apiTimeout = opts.apiTimeout;
|
||||
if (!(uuid && apiTimeout)) {
|
||||
throw new InternalInconsistencyError(
|
||||
'UUID and apiTimeout should be defined in exchangeKeyAndGetDevice',
|
||||
);
|
||||
}
|
||||
|
||||
// If we have an existing device key we first check if it's
|
||||
// valid, because if it is then we can just use that
|
||||
if (opts.deviceApiKey != null) {
|
||||
try {
|
||||
return await fetchDevice(balenaApi, uuid, opts.deviceApiKey, apiTimeout);
|
||||
} catch (e) {
|
||||
if (e instanceof DeviceNotFoundError) {
|
||||
// do nothing...
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not valid or doesn't exist then we try to use the
|
||||
// user/provisioning api key for the exchange
|
||||
if (!opts.provisioningApiKey) {
|
||||
throw new InternalInconsistencyError(
|
||||
'Required a provisioning key in exchangeKeyAndGetDevice',
|
||||
);
|
||||
}
|
||||
|
||||
let device: Device;
|
||||
try {
|
||||
device = await fetchDevice(
|
||||
balenaApi,
|
||||
uuid,
|
||||
opts.provisioningApiKey,
|
||||
apiTimeout,
|
||||
);
|
||||
} catch (err) {
|
||||
throw new ExchangeKeyError(`Couldn't fetch device with provisioning key`);
|
||||
}
|
||||
|
||||
// We found the device so we can try to register a working device key for it
|
||||
const [res] = await (await request.getRequestInstance())
|
||||
.postAsync(`${opts.apiEndpoint}/api-key/device/${device.id}/device-key`, {
|
||||
json: true,
|
||||
body: {
|
||||
apiKey: opts.deviceApiKey,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${opts.provisioningApiKey}`,
|
||||
},
|
||||
})
|
||||
.timeout(apiTimeout);
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
throw new ExchangeKeyError(
|
||||
`Couldn't register device key with provisioning key`,
|
||||
);
|
||||
}
|
||||
|
||||
return device;
|
||||
};
|
||||
|
||||
export const provision = async (
|
||||
balenaApi: PinejsClientRequest,
|
||||
opts: KeyExchangeOpts,
|
||||
) => {
|
||||
await config.initialized;
|
||||
await eventTracker.initialized;
|
||||
|
||||
let device: Device | null = null;
|
||||
|
||||
if (
|
||||
opts.registered_at == null ||
|
||||
opts.deviceId == null ||
|
||||
opts.provisioningApiKey != null
|
||||
) {
|
||||
if (opts.registered_at != null && opts.deviceId == null) {
|
||||
log.debug(
|
||||
'Device is registered but no device id available, attempting key exchange',
|
||||
);
|
||||
|
||||
device = await exchangeKeyAndGetDeviceOrRegenerate(balenaApi, opts);
|
||||
} else if (opts.registered_at == null) {
|
||||
log.info('New device detected. Provisioning...');
|
||||
try {
|
||||
device = await deviceRegister.register(opts).timeout(opts.apiTimeout);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof deviceRegister.ApiError &&
|
||||
isHttpConflictError(err.response)
|
||||
) {
|
||||
log.debug('UUID already registered, trying a key exchange');
|
||||
device = await exchangeKeyAndGetDeviceOrRegenerate(balenaApi, opts);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
opts.registered_at = Date.now();
|
||||
} else if (opts.provisioningApiKey != null) {
|
||||
log.debug(
|
||||
'Device is registered but we still have an apiKey, attempting key exchange',
|
||||
);
|
||||
device = await exchangeKeyAndGetDevice(balenaApi, opts);
|
||||
}
|
||||
|
||||
if (!device) {
|
||||
throw new FailedToProvisionDeviceError();
|
||||
}
|
||||
|
||||
const { id } = device;
|
||||
balenaApi.passthrough.headers.Authorization = `Bearer ${opts.deviceApiKey}`;
|
||||
|
||||
const configToUpdate = {
|
||||
registered_at: opts.registered_at,
|
||||
deviceId: id,
|
||||
apiKey: null,
|
||||
};
|
||||
|
||||
await config.set(configToUpdate);
|
||||
eventTracker.track('Device bootstrap success');
|
||||
}
|
||||
|
||||
return device;
|
||||
};
|
@ -14,6 +14,8 @@ interface CodedSysError extends Error {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
export class DeviceNotFoundError extends TypedError {}
|
||||
|
||||
export function NotFoundError(err: StatusCodeError): boolean {
|
||||
return checkInt(err.statusCode) === 404;
|
||||
}
|
||||
@ -50,6 +52,12 @@ export function isHttpConflictError(err: StatusCodeError | Response): boolean {
|
||||
return checkInt(err.statusCode) === 409;
|
||||
}
|
||||
|
||||
export class FailedToProvisionDeviceError extends TypedError {
|
||||
public constructor() {
|
||||
super('Failed to provision device');
|
||||
}
|
||||
}
|
||||
|
||||
export class ExchangeKeyError extends TypedError {}
|
||||
|
||||
export class InternalInconsistencyError extends TypedError {}
|
||||
|
@ -20,6 +20,7 @@ import * as db from './db';
|
||||
import * as config from './config';
|
||||
import * as dockerUtils from './lib/docker-utils';
|
||||
import * as logger from './logger';
|
||||
import * as apiHelper from './lib/api-helper';
|
||||
|
||||
const mkdirpAsync = Promise.promisify(mkdirp);
|
||||
|
||||
@ -406,8 +407,8 @@ export class Proxyvisor {
|
||||
}
|
||||
// If the device is not in the DB it means it was provisioned externally
|
||||
// so we need to fetch it.
|
||||
return this.apiBinder
|
||||
.fetchDevice(uuid, currentApiKey, apiTimeout)
|
||||
return apiHelper
|
||||
.fetchDevice(this.apiBinder.balenaApi, uuid, currentApiKey, apiTimeout)
|
||||
.then((dev) => {
|
||||
const deviceForDB = {
|
||||
uuid,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import APIBinder from './api-binder';
|
||||
import * as apiBinder from './api-binder';
|
||||
import * as db from './db';
|
||||
import * as config from './config';
|
||||
import DeviceState from './device-state';
|
||||
@ -32,25 +32,24 @@ const startupConfigFields: config.ConfigKey[] = [
|
||||
|
||||
export class Supervisor {
|
||||
private deviceState: DeviceState;
|
||||
private apiBinder: APIBinder;
|
||||
private api: SupervisorAPI;
|
||||
|
||||
public constructor() {
|
||||
this.apiBinder = new APIBinder();
|
||||
this.deviceState = new DeviceState({
|
||||
apiBinder: this.apiBinder,
|
||||
apiBinder,
|
||||
});
|
||||
|
||||
// workaround the circular dependency
|
||||
this.apiBinder.setDeviceState(this.deviceState);
|
||||
apiBinder.setDeviceState(this.deviceState);
|
||||
|
||||
// FIXME: rearchitect proxyvisor to avoid this circular dependency
|
||||
// by storing current state and having the APIBinder query and report it / provision devices
|
||||
this.deviceState.applications.proxyvisor.bindToAPI(this.apiBinder);
|
||||
this.deviceState.applications.proxyvisor.bindToAPI(apiBinder);
|
||||
|
||||
this.api = new SupervisorAPI({
|
||||
routers: [this.apiBinder.router, this.deviceState.router],
|
||||
routers: [apiBinder.router, this.deviceState.router],
|
||||
healthchecks: [
|
||||
this.apiBinder.healthcheck.bind(this.apiBinder),
|
||||
apiBinder.healthcheck,
|
||||
this.deviceState.healthcheck.bind(this.deviceState),
|
||||
],
|
||||
});
|
||||
@ -78,14 +77,14 @@ export class Supervisor {
|
||||
await firewall.initialised;
|
||||
|
||||
log.debug('Starting api binder');
|
||||
await this.apiBinder.initClient();
|
||||
await apiBinder.initialized;
|
||||
|
||||
logger.logSystemMessage('Supervisor starting', {}, 'Supervisor start');
|
||||
if (conf.legacyAppsPresent && this.apiBinder.balenaApi != null) {
|
||||
if (conf.legacyAppsPresent && apiBinder.balenaApi != null) {
|
||||
log.info('Legacy app detected, running migration');
|
||||
await normaliseLegacyDatabase(
|
||||
this.deviceState.applications,
|
||||
this.apiBinder.balenaApi,
|
||||
apiBinder.balenaApi,
|
||||
);
|
||||
}
|
||||
|
||||
@ -95,7 +94,7 @@ export class Supervisor {
|
||||
this.api.listen(conf.listenPort, conf.apiTimeout);
|
||||
this.deviceState.on('shutdown', () => this.api.stop());
|
||||
|
||||
await this.apiBinder.start();
|
||||
await apiBinder.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ process.env.DATABASE_PATH_3 = './test/data/database3.sqlite';
|
||||
process.env.LED_FILE = './test/data/led_file';
|
||||
|
||||
import './lib/mocked-iptables';
|
||||
import './lib/mocked-event-tracker';
|
||||
|
||||
import * as dbus from 'dbus';
|
||||
import { DBusError, DBusInterface } from 'dbus';
|
||||
|
@ -3,11 +3,9 @@ import { fs } from 'mz';
|
||||
import { Server } from 'net';
|
||||
import { SinonSpy, SinonStub, spy, stub } from 'sinon';
|
||||
|
||||
import ApiBinder from '../src/api-binder';
|
||||
import prepare = require('./lib/prepare');
|
||||
import * as config from '../src/config';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as eventTracker from '../src/event-tracker';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import chai = require('./lib/chai-config');
|
||||
import balenaAPI = require('./lib/mocked-balena-api');
|
||||
@ -16,15 +14,25 @@ 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 { TypedError } from 'typed-error';
|
||||
import { DeviceNotFoundError } from '../src/lib/errors';
|
||||
|
||||
import { eventTrackSpy } from './lib/mocked-event-tracker';
|
||||
|
||||
const { expect } = chai;
|
||||
let ApiBinder: typeof import('../src/api-binder');
|
||||
|
||||
class ExpectedError extends TypedError {}
|
||||
|
||||
const defaultConfigBackend = config.configJsonBackend;
|
||||
const initModels = async (obj: Dictionary<any>, filename: string) => {
|
||||
await prepare();
|
||||
|
||||
// @ts-expect-error setting read-only property
|
||||
config.configJsonBackend = new ConfigJsonConfigBackend(schema, filename);
|
||||
config.generateRequiredFields();
|
||||
await config.generateRequiredFields();
|
||||
|
||||
// @ts-expect-error using private properties
|
||||
config.configJsonBackend.cache = await config.configJsonBackend.read();
|
||||
await config.generateRequiredFields();
|
||||
@ -35,15 +43,15 @@ const initModels = async (obj: Dictionary<any>, filename: string) => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
obj.apiBinder = new ApiBinder();
|
||||
ApiBinder = await import('../src/api-binder');
|
||||
await ApiBinder.initialized;
|
||||
obj.apiBinder = ApiBinder;
|
||||
|
||||
obj.deviceState = new DeviceState({
|
||||
apiBinder: obj.apiBinder,
|
||||
});
|
||||
|
||||
obj.apiBinder.setDeviceState(obj.deviceState);
|
||||
|
||||
await obj.apiBinder.initClient(); // Initializes the clients but doesn't trigger provisioning
|
||||
};
|
||||
|
||||
const mockProvisioningOpts = {
|
||||
@ -55,17 +63,12 @@ const mockProvisioningOpts = {
|
||||
};
|
||||
|
||||
describe('ApiBinder', () => {
|
||||
const defaultConfigBackend = config.configJsonBackend;
|
||||
let server: Server;
|
||||
|
||||
beforeEach(() => {
|
||||
stub(eventTracker, 'track');
|
||||
});
|
||||
afterEach(() => {
|
||||
// @ts-expect-error Restoring a non-stub type function
|
||||
eventTracker.track.restore();
|
||||
});
|
||||
before(async () => {
|
||||
delete require.cache[require.resolve('../src/api-binder')];
|
||||
|
||||
before(() => {
|
||||
spy(balenaAPI.balenaBackend!, 'registerHandler');
|
||||
server = balenaAPI.listen(3000);
|
||||
});
|
||||
@ -83,31 +86,39 @@ describe('ApiBinder', () => {
|
||||
// We do not support older OS versions anymore, so we only test this case
|
||||
describe('on an OS with deviceApiKey support', () => {
|
||||
const components: Dictionary<any> = {};
|
||||
before(() => {
|
||||
return initModels(components, '/config-apibinder.json');
|
||||
|
||||
before(async () => {
|
||||
await initModels(components, '/config-apibinder.json');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
eventTrackSpy.resetHistory();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
eventTrackSpy.restore();
|
||||
|
||||
// @ts-expect-error setting read-only property
|
||||
config.configJsonBackend = defaultConfigBackend;
|
||||
await config.generateRequiredFields();
|
||||
});
|
||||
|
||||
it('provisions a device', () => {
|
||||
const promise = components.apiBinder.provisionDevice();
|
||||
it('provisions a device', async () => {
|
||||
const opts = await config.get('provisioningOptions');
|
||||
await ApiHelper.provision(components.apiBinder.balenaApi, opts);
|
||||
|
||||
return expect(promise).to.be.fulfilled.then(() => {
|
||||
expect(balenaAPI.balenaBackend!.registerHandler).to.be.calledOnce;
|
||||
expect(balenaAPI.balenaBackend!.registerHandler).to.be.calledOnce;
|
||||
expect(eventTrackSpy).to.be.called;
|
||||
expect(eventTrackSpy).to.be.calledWith('Device bootstrap success');
|
||||
|
||||
// @ts-expect-error function does not exist on type
|
||||
balenaAPI.balenaBackend!.registerHandler.resetHistory();
|
||||
expect(eventTracker.track).to.be.calledWith('Device bootstrap success');
|
||||
});
|
||||
// @ts-expect-error function does not exist on type
|
||||
balenaAPI.balenaBackend!.registerHandler.resetHistory();
|
||||
});
|
||||
|
||||
it('exchanges keys if resource conflict when provisioning', async () => {
|
||||
// Get current config to extend
|
||||
const currentConfig = await config.get('provisioningOptions');
|
||||
|
||||
// Stub config values so we have correct conditions
|
||||
const configStub = stub(config, 'get').resolves({
|
||||
...currentConfig,
|
||||
@ -115,30 +126,32 @@ describe('ApiBinder', () => {
|
||||
provisioningApiKey: '123', // Previous test case deleted the provisioningApiKey so add one
|
||||
uuid: 'not-unique', // This UUID is used in mocked-balena-api as an existing registered UUID
|
||||
});
|
||||
|
||||
// If api-binder reaches this function then tests pass
|
||||
// We throw an error so we don't have to keep stubbing
|
||||
const functionToReach = stub(
|
||||
components.apiBinder,
|
||||
ApiHelper,
|
||||
'exchangeKeyAndGetDeviceOrRegenerate',
|
||||
).rejects('expected-rejection'); // We throw an error so we don't have to keep stubbing
|
||||
).throws(new ExpectedError());
|
||||
|
||||
spy(Log, 'debug');
|
||||
|
||||
try {
|
||||
await components.apiBinder.provision();
|
||||
const opts = await config.get('provisioningOptions');
|
||||
await ApiHelper.provision(components.apiBinder.balenaApi, opts);
|
||||
} catch (e) {
|
||||
// Check that the error thrown is from this test
|
||||
if (e.name !== 'expected-rejection') {
|
||||
throw e;
|
||||
}
|
||||
expect(e).to.be.instanceOf(ExpectedError);
|
||||
}
|
||||
|
||||
expect(functionToReach).to.be.calledOnce;
|
||||
expect(functionToReach.called).to.be.true;
|
||||
expect((Log.debug as SinonSpy).lastCall.lastArg).to.equal(
|
||||
'UUID already registered, trying a key exchange',
|
||||
);
|
||||
|
||||
// Restore stubs
|
||||
functionToReach.restore();
|
||||
configStub.restore();
|
||||
functionToReach.restore();
|
||||
(Log.debug as SinonStub).restore();
|
||||
});
|
||||
|
||||
@ -184,7 +197,8 @@ describe('ApiBinder', () => {
|
||||
api_key: 'verysecure',
|
||||
};
|
||||
|
||||
const device = await components.apiBinder.fetchDevice(
|
||||
const device = await ApiHelper.fetchDevice(
|
||||
components.apiBinder.balenaApi,
|
||||
'abcd',
|
||||
'someApiKey',
|
||||
30000,
|
||||
@ -207,27 +221,31 @@ describe('ApiBinder', () => {
|
||||
it('returns the device if it can fetch it with the deviceApiKey', async () => {
|
||||
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
|
||||
|
||||
const fetchDeviceStub = stub(components.apiBinder, 'fetchDevice');
|
||||
const fetchDeviceStub = stub(ApiHelper, 'fetchDevice');
|
||||
fetchDeviceStub.onCall(0).resolves({ id: 1 });
|
||||
|
||||
const device = await components.apiBinder.exchangeKeyAndGetDevice(
|
||||
const device = await ApiHelper.exchangeKeyAndGetDevice(
|
||||
components.apiBinder.balenaApi,
|
||||
mockProvisioningOpts,
|
||||
);
|
||||
|
||||
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.not.be.called;
|
||||
expect(device).to.deep.equal({ id: 1 });
|
||||
expect(components.apiBinder.fetchDevice).to.be.calledOnce;
|
||||
expect(fetchDeviceStub).to.be.calledOnce;
|
||||
|
||||
components.apiBinder.fetchDevice.restore();
|
||||
// @ts-expect-error function does not exist on type
|
||||
balenaAPI.balenaBackend.deviceKeyHandler.restore();
|
||||
fetchDeviceStub.restore();
|
||||
});
|
||||
|
||||
it('throws if it cannot get the device with any of the keys', () => {
|
||||
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
|
||||
stub(components.apiBinder, 'fetchDevice').returns(Promise.resolve(null));
|
||||
const fetchDeviceStub = stub(ApiHelper, 'fetchDevice').throws(
|
||||
new DeviceNotFoundError(),
|
||||
);
|
||||
|
||||
const promise = components.apiBinder.exchangeKeyAndGetDevice(
|
||||
const promise = ApiHelper.exchangeKeyAndGetDevice(
|
||||
components.apiBinder.balenaApi,
|
||||
mockProvisioningOpts,
|
||||
);
|
||||
promise.catch(() => {
|
||||
@ -236,8 +254,8 @@ describe('ApiBinder', () => {
|
||||
|
||||
return expect(promise).to.be.rejected.then(() => {
|
||||
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.not.be.called;
|
||||
expect(components.apiBinder.fetchDevice).to.be.calledTwice;
|
||||
components.apiBinder.fetchDevice.restore();
|
||||
expect(fetchDeviceStub).to.be.calledTwice;
|
||||
fetchDeviceStub.restore();
|
||||
// @ts-expect-error function does not exist on type
|
||||
balenaAPI.balenaBackend.deviceKeyHandler.restore();
|
||||
});
|
||||
@ -245,17 +263,18 @@ describe('ApiBinder', () => {
|
||||
|
||||
it('exchanges the key and returns the device if the provisioning key is valid', async () => {
|
||||
spy(balenaAPI.balenaBackend!, 'deviceKeyHandler');
|
||||
const fetchDeviceStub = stub(components.apiBinder, 'fetchDevice');
|
||||
fetchDeviceStub.onCall(0).returns(Promise.resolve(null));
|
||||
const fetchDeviceStub = stub(ApiHelper, 'fetchDevice');
|
||||
fetchDeviceStub.onCall(0).throws(new DeviceNotFoundError());
|
||||
fetchDeviceStub.onCall(1).returns(Promise.resolve({ id: 1 }));
|
||||
|
||||
const device = await components.apiBinder.exchangeKeyAndGetDevice(
|
||||
const device = await ApiHelper.exchangeKeyAndGetDevice(
|
||||
components.apiBinder.balenaApi,
|
||||
mockProvisioningOpts as any,
|
||||
);
|
||||
expect(balenaAPI.balenaBackend!.deviceKeyHandler).to.be.calledOnce;
|
||||
expect(device).to.deep.equal({ id: 1 });
|
||||
expect(components.apiBinder.fetchDevice).to.be.calledTwice;
|
||||
components.apiBinder.fetchDevice.restore();
|
||||
expect(fetchDeviceStub).to.be.calledTwice;
|
||||
fetchDeviceStub.restore();
|
||||
// @ts-expect-error function does not exist on type
|
||||
balenaAPI.balenaBackend.deviceKeyHandler.restore();
|
||||
});
|
||||
@ -436,17 +455,22 @@ describe('ApiBinder', () => {
|
||||
appUpdatePollInterval: 1000,
|
||||
connectivityCheckEnabled: true,
|
||||
});
|
||||
|
||||
// Set lastFetch to now so it is within appUpdatePollInterval
|
||||
(TargetState as any).lastFetch = process.hrtime();
|
||||
|
||||
// Copy previous values to restore later
|
||||
const previousStateReportErrors = components.apiBinder.stateReportErrors;
|
||||
const previousDeviceStateConnected =
|
||||
components.apiBinder.deviceState.connected;
|
||||
|
||||
// Set additional conditions not in configStub to cause a fail
|
||||
// @ts-expect-error
|
||||
CurrentState.stateReportErrors = 4;
|
||||
components.apiBinder.deviceState.connected = true;
|
||||
|
||||
expect(await components.apiBinder.healthcheck()).to.equal(false);
|
||||
|
||||
expect(Log.info).to.be.calledOnce;
|
||||
expect((Log.info as SinonSpy).lastCall?.lastArg).to.equal(
|
||||
stripIndent`
|
||||
@ -455,6 +479,7 @@ describe('ApiBinder', () => {
|
||||
- device state is disconnected ? false
|
||||
- stateReportErrors less then 3 ? false`,
|
||||
);
|
||||
|
||||
// Restore previous values
|
||||
// @ts-expect-error
|
||||
CurrentState.stateReportErrors = previousStateReportErrors;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SinonStub, stub } from 'sinon';
|
||||
|
||||
import APIBinder from '../src/api-binder';
|
||||
import * as APIBinder from '../src/api-binder';
|
||||
import { ApplicationManager } from '../src/application-manager';
|
||||
import DeviceState from '../src/device-state';
|
||||
import * as constants from '../src/lib/constants';
|
||||
@ -10,7 +10,6 @@ import { expect } from './lib/chai-config';
|
||||
import _ = require('lodash');
|
||||
|
||||
describe('Startup', () => {
|
||||
let initClientStub: SinonStub;
|
||||
let reportCurrentStateStub: SinonStub;
|
||||
let startStub: SinonStub;
|
||||
let vpnStatusPathStub: SinonStub;
|
||||
@ -19,16 +18,11 @@ describe('Startup', () => {
|
||||
let dockerStub: SinonStub;
|
||||
|
||||
before(() => {
|
||||
initClientStub = stub(APIBinder.prototype as any, 'initClient').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
reportCurrentStateStub = stub(
|
||||
DeviceState.prototype as any,
|
||||
'reportCurrentState',
|
||||
).resolves();
|
||||
startStub = stub(APIBinder.prototype as any, 'start').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
startStub = stub(APIBinder as any, 'start').returns(Promise.resolve());
|
||||
appManagerStub = stub(ApplicationManager.prototype, 'init').returns(
|
||||
Promise.resolve(),
|
||||
);
|
||||
@ -40,7 +34,6 @@ describe('Startup', () => {
|
||||
});
|
||||
|
||||
after(() => {
|
||||
initClientStub.restore();
|
||||
startStub.restore();
|
||||
appManagerStub.restore();
|
||||
vpnStatusPathStub.restore();
|
||||
|
@ -2,7 +2,7 @@ import { expect } from 'chai';
|
||||
import { spy, stub, SinonStub } from 'sinon';
|
||||
import * as supertest from 'supertest';
|
||||
|
||||
import APIBinder from '../src/api-binder';
|
||||
import * as apiBinder from '../src/api-binder';
|
||||
import DeviceState from '../src/device-state';
|
||||
import Log from '../src/lib/supervisor-console';
|
||||
import * as images from '../src/compose/images';
|
||||
@ -24,9 +24,11 @@ describe('SupervisorAPI', () => {
|
||||
const originalGetStatus = images.getStatus;
|
||||
|
||||
before(async () => {
|
||||
await apiBinder.initialized;
|
||||
|
||||
// Stub health checks so we can modify them whenever needed
|
||||
healthCheckStubs = [
|
||||
stub(APIBinder.prototype, 'healthcheck'),
|
||||
stub(apiBinder, 'healthcheck'),
|
||||
stub(DeviceState.prototype, 'healthcheck'),
|
||||
];
|
||||
// The mockedAPI contains stubs that might create unexpected results
|
||||
@ -37,7 +39,7 @@ describe('SupervisorAPI', () => {
|
||||
images.getStatus = () => Promise.resolve([]);
|
||||
|
||||
// Start test API
|
||||
return api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
await api.listen(mockedOptions.listenPort, mockedOptions.timeout);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -52,6 +52,12 @@ api.get(/\/v6\/device\(uuid=%27([0-9a-f]+)%27\)/, (req, res) =>
|
||||
api.balenaBackend!.getDeviceHandler(req, res, _.noop),
|
||||
);
|
||||
|
||||
api.get(/\/v6\/device/, (req, res) => {
|
||||
const [, uuid] = /uuid eq '([0-9a-f]+)'/i.exec(req.query['$filter']) ?? [];
|
||||
req.params[0] = uuid;
|
||||
return api.balenaBackend!.getDeviceHandler(req, res, _.noop);
|
||||
});
|
||||
|
||||
api.post('/api-key/device/:deviceId/device-key', (req, res) =>
|
||||
api.balenaBackend!.deviceKeyHandler(req, res, _.noop),
|
||||
);
|
||||
|
@ -10,7 +10,7 @@ import * as config from '../../src/config';
|
||||
import * as db from '../../src/db';
|
||||
import { createV1Api } from '../../src/device-api/v1';
|
||||
import { createV2Api } from '../../src/device-api/v2';
|
||||
import APIBinder from '../../src/api-binder';
|
||||
import * as APIBinder from '../../src/api-binder';
|
||||
import DeviceState from '../../src/device-state';
|
||||
import SupervisorAPI from '../../src/supervisor-api';
|
||||
|
||||
@ -109,7 +109,7 @@ async function createAPIOpts(): Promise<SupervisorAPIOpts> {
|
||||
const deviceState = new DeviceState({
|
||||
apiBinder: null as any,
|
||||
});
|
||||
const apiBinder = new APIBinder();
|
||||
const apiBinder = APIBinder;
|
||||
return {
|
||||
deviceState,
|
||||
apiBinder,
|
||||
@ -170,7 +170,7 @@ function restoreStubs() {
|
||||
|
||||
interface SupervisorAPIOpts {
|
||||
deviceState: DeviceState;
|
||||
apiBinder: APIBinder;
|
||||
apiBinder: typeof APIBinder;
|
||||
}
|
||||
|
||||
export = { create, cleanUp, STUBBED_VALUES };
|
||||
|
4
test/lib/mocked-event-tracker.ts
Normal file
4
test/lib/mocked-event-tracker.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import * as eventTracker from '../../src/event-tracker';
|
||||
import { spy } from 'sinon';
|
||||
|
||||
export const eventTrackSpy = spy(eventTracker, 'track');
|
Loading…
Reference in New Issue
Block a user