mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-20 06:07:57 +00:00
48e0733c7e
The supervisor uses the following pattern for async module initialization ```typescript // module.ts export const initialised = (async () => { // do some async initialization })(); // somewhere else import * as module from 'module'; async function setup() { await module.initialise; } ``` The above pattern means that whenever the module is imported, the initialisation procedure will be ran, which is an anti-pattern. This converts any instance of this pattern into a function ```typescript export const initialised = _.once(async () => { // do some async initialization }); ``` And anywhere else on the code it replaces the call with a ```typescript await module.initialised(); ``` Change-type: patch
258 lines
7.4 KiB
TypeScript
258 lines
7.4 KiB
TypeScript
import * as _ from 'lodash';
|
|
import { promises as fs } from 'fs';
|
|
import { SinonStub, stub } from 'sinon';
|
|
import { expect } from 'chai';
|
|
|
|
import prepare = require('~/test-lib/prepare');
|
|
import * as conf from '~/src/config';
|
|
|
|
import constants = require('~/lib/constants');
|
|
import { SchemaTypeKey } from '~/src/config/schema-type';
|
|
import { fnSchema } from '~/src/config/functions';
|
|
|
|
describe('Config', () => {
|
|
before(async () => {
|
|
await prepare();
|
|
await conf.initialized();
|
|
});
|
|
|
|
it('reads and exposes values from the config.json', async () => {
|
|
const id = await conf.get('applicationId');
|
|
return expect(id).to.equal(78373);
|
|
});
|
|
|
|
it('allows reading several values in one getMany call', async () => {
|
|
return expect(
|
|
await conf.getMany(['applicationId', 'apiEndpoint']),
|
|
).to.deep.equal({
|
|
applicationId: 78373,
|
|
apiEndpoint: 'https://api.resin.io',
|
|
});
|
|
});
|
|
|
|
it('generates a uuid and stores it in config.json', async () => {
|
|
const uuid = await conf.get('uuid');
|
|
const configJsonUuid = JSON.parse(
|
|
await fs.readFile('./test/data/config.json', 'utf8'),
|
|
).uuid;
|
|
expect(uuid).to.be.a('string');
|
|
expect(uuid).to.have.lengthOf(32);
|
|
expect(uuid).to.equal(configJsonUuid);
|
|
});
|
|
|
|
it('does not allow setting an immutable field', async () => {
|
|
const promise = conf.set({ deviceType: 'a different device type' });
|
|
// We catch it to avoid the unhandled error log
|
|
promise.catch(_.noop);
|
|
return expect(promise).to.be.rejected;
|
|
});
|
|
|
|
it('allows setting both config.json and database fields transparently', async () => {
|
|
await conf.set({ appUpdatePollInterval: 30000, name: 'a new device name' });
|
|
const config = await conf.getMany(['appUpdatePollInterval', 'name']);
|
|
return expect(config).to.deep.equal({
|
|
appUpdatePollInterval: 30000,
|
|
name: 'a new device name',
|
|
});
|
|
});
|
|
|
|
it('allows deleting a config.json key and returns a default value if none is set', async () => {
|
|
await conf.remove('appUpdatePollInterval');
|
|
const poll = await conf.get('appUpdatePollInterval');
|
|
return expect(poll).to.equal(900000);
|
|
});
|
|
|
|
it('allows deleting a config.json key if it is null', async () => {
|
|
await conf.set({ apiKey: null });
|
|
const key = await conf.get('apiKey');
|
|
|
|
expect(key).to.be.undefined;
|
|
expect(
|
|
JSON.parse(await fs.readFile('./test/data/config.json', 'utf8')),
|
|
).to.not.have.property('apiKey');
|
|
});
|
|
|
|
it('does not allow modifying or removing a function value', () => {
|
|
// We have to cast to any below, as the type system will
|
|
// not allow removing a function value
|
|
expect(conf.remove('version' as any)).to.be.rejected;
|
|
expect(conf.set({ version: '2.0' })).to.be.rejected;
|
|
});
|
|
|
|
it('throws when asked for an unknown key', () => {
|
|
expect(conf.get('unknownInvalidValue' as any)).to.be.rejected;
|
|
});
|
|
|
|
it('emits a change event when values', (done) => {
|
|
const listener = (val: conf.ConfigChangeMap<SchemaTypeKey>) => {
|
|
try {
|
|
if ('name' in val) {
|
|
expect(val.name).to.equal('someValue');
|
|
done();
|
|
conf.removeListener('change', listener);
|
|
}
|
|
} catch (e) {
|
|
done(e);
|
|
}
|
|
};
|
|
conf.on('change', listener);
|
|
conf.set({ name: 'someValue' });
|
|
});
|
|
|
|
it("returns production OS variant if it doesn't exist", async () => {
|
|
const oldPath = constants.hostOSVersionPath;
|
|
constants.hostOSVersionPath = 'test/data/etc/os-release-novariant';
|
|
|
|
const osVariant = await conf.get('osVariant');
|
|
constants.hostOSVersionPath = oldPath;
|
|
expect(osVariant).to.equal('prod');
|
|
});
|
|
|
|
it('reads and exposes MAC addresses', async () => {
|
|
const macAddress = await conf.get('macAddress');
|
|
expect(macAddress).to.have.length.greaterThan(0);
|
|
});
|
|
|
|
describe('Function config providers', () => {
|
|
it('should throw if a non-mutable function provider is set', () => {
|
|
expect(conf.set({ version: 'some-version' })).to.be.rejected;
|
|
});
|
|
|
|
it('should throw if a non-mutable function provider is removed', () => {
|
|
expect(conf.remove('version' as any)).to.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Config data sources', () => {
|
|
afterEach(() => {
|
|
// Clean up memoized values
|
|
fnSchema.deviceArch.clear();
|
|
fnSchema.deviceType.clear();
|
|
});
|
|
|
|
it('should obtain deviceArch from device-type.json', async () => {
|
|
const [slug, arch] = ['raspberrypi3', 'armv7hf'];
|
|
stub(fs, 'readFile').resolves(
|
|
JSON.stringify({
|
|
slug,
|
|
arch,
|
|
}),
|
|
);
|
|
|
|
const deviceArch = await conf.get('deviceArch');
|
|
expect(deviceArch).to.equal(arch);
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
expect(fs.readFile).to.be.calledWith(
|
|
`${constants.rootMountPoint}${constants.bootMountPoint}/device-type.json`,
|
|
'utf8',
|
|
);
|
|
|
|
(fs.readFile as SinonStub).restore();
|
|
});
|
|
|
|
it('should obtain deviceType from device-type.json', async () => {
|
|
const [slug, arch] = ['raspberrypi3', 'armv7hf'];
|
|
stub(fs, 'readFile').resolves(
|
|
JSON.stringify({
|
|
slug,
|
|
arch,
|
|
}),
|
|
);
|
|
|
|
const deviceType = await conf.get('deviceType');
|
|
expect(deviceType).to.equal(slug);
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
expect(fs.readFile).to.be.calledWith(
|
|
`${constants.rootMountPoint}${constants.bootMountPoint}/device-type.json`,
|
|
'utf8',
|
|
);
|
|
|
|
(fs.readFile as SinonStub).restore();
|
|
});
|
|
|
|
it('should memoize values from device-type.json', async () => {
|
|
const [slug, arch] = ['raspberrypi3', 'armv7hf'];
|
|
stub(fs, 'readFile').resolves(
|
|
JSON.stringify({
|
|
slug,
|
|
arch,
|
|
}),
|
|
);
|
|
|
|
// Make a first call to get the value to be memoized
|
|
await conf.get('deviceType');
|
|
await conf.get('deviceArch');
|
|
expect(fs.readFile).to.be.called;
|
|
(fs.readFile as SinonStub).resetHistory();
|
|
|
|
const deviceArch = await conf.get('deviceArch');
|
|
expect(deviceArch).to.equal(arch);
|
|
|
|
// The result should still be memoized from the previous call
|
|
expect(fs.readFile).to.not.be.called;
|
|
|
|
const deviceType = await conf.get('deviceType');
|
|
expect(deviceType).to.equal(slug);
|
|
|
|
// The result should still be memoized from the previous call
|
|
expect(fs.readFile).to.not.be.called;
|
|
|
|
(fs.readFile as SinonStub).restore();
|
|
});
|
|
|
|
it('should not memoize errors when reading deviceArch', (done) => {
|
|
// File not found
|
|
stub(fs, 'readFile').throws('File not found');
|
|
|
|
expect(conf.get('deviceArch')).to.eventually.equal('unknown');
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
(fs.readFile as SinonStub).restore();
|
|
|
|
// Next call should not throw
|
|
const [slug, arch] = ['raspberrypi3', 'armv7hf'];
|
|
stub(fs, 'readFile').resolves(
|
|
JSON.stringify({
|
|
slug,
|
|
arch,
|
|
}),
|
|
);
|
|
|
|
// We need to let rejection be discovered
|
|
// https://github.com/medikoo/memoizee/issues/93
|
|
setTimeout(() => {
|
|
expect(conf.get('deviceArch')).to.eventually.equal(arch);
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
(fs.readFile as SinonStub).restore();
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not memoize errors when reading deviceType', (done) => {
|
|
// File not found
|
|
stub(fs, 'readFile').throws('File not found');
|
|
|
|
expect(conf.get('deviceType')).to.eventually.equal('unknown');
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
(fs.readFile as SinonStub).restore();
|
|
|
|
// Next call should not throw
|
|
const [slug, arch] = ['raspberrypi3', 'armv7hf'];
|
|
stub(fs, 'readFile').resolves(
|
|
JSON.stringify({
|
|
slug,
|
|
arch,
|
|
}),
|
|
);
|
|
|
|
// We need to let rejection be discovered
|
|
// https://github.com/medikoo/memoizee/issues/93
|
|
setTimeout(() => {
|
|
expect(conf.get('deviceType')).to.eventually.equal(slug);
|
|
expect(fs.readFile).to.be.calledOnce;
|
|
(fs.readFile as SinonStub).restore();
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|