Add support for BALENA_HOST_SPLASH_IMAGE config

Setting this this variable to a base64 encoded string will replace the splash
image on the device by rewriting `/mnt/boot/splash/balena-logo.png`.
This will also make a copy of the default balena logo so the splash can
be restored if the variable is removed.

Change-type: minor
Signed-off-by: Felipe Lalanne <felipe@balena.io>
This commit is contained in:
Felipe Lalanne 2021-01-05 18:37:53 -03:00
parent e66a775c15
commit 4aa8090a56
8 changed files with 696 additions and 10 deletions

View File

@ -12,7 +12,7 @@ export const bootMountPoint = `${constants.rootMountPoint}${constants.bootMountP
export async function remountAndWriteAtomic(
file: string,
data: string,
data: string | Buffer,
): Promise<void> {
// Here's the dangerous part:
await child_process.exec(

View File

@ -5,6 +5,7 @@ import { ExtraUEnv } from './extra-uEnv';
import { ConfigTxt } from './config-txt';
import { ConfigFs } from './config-fs';
import { Odmdata } from './odmdata';
import { SplashImage } from './splash-image';
export const allBackends = [
new Extlinux(),
@ -12,6 +13,7 @@ export const allBackends = [
new ConfigTxt(),
new ConfigFs(),
new Odmdata(),
new SplashImage(),
];
export function matchesAnyBootConfig(envVar: string): boolean {

View File

@ -0,0 +1,206 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { fs } from 'mz';
import * as path from 'path';
import * as constants from '../../lib/constants';
import log from '../../lib/supervisor-console';
import {
bootMountPoint,
ConfigBackend,
ConfigOptions,
remountAndWriteAtomic,
} from './backend';
export class SplashImage extends ConfigBackend {
private static readonly BASEPATH = path.join(bootMountPoint, 'splash');
private static readonly DEFAULT = path.join(
SplashImage.BASEPATH,
'balena-logo-default.png',
);
private static readonly FILENAMES = ['balena-logo.png', 'resin-logo.png'];
private static readonly PREFIX = `${constants.hostConfigVarPrefix}SPLASH_`;
private static readonly CONFIGS = ['image'];
private static readonly DATA_URI_REGEX = /^data:(.+);base64,(.*)$/;
// Check the first 8 bytes of a buffer for a PNG header
// Source: https://github.com/sindresorhus/is-png/blob/master/index.js
private isPng(buffer: Buffer) {
if (!buffer || buffer.length < 8) {
return false;
}
return (
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4e &&
buffer[3] === 0x47 &&
buffer[4] === 0x0d &&
buffer[5] === 0x0a &&
buffer[6] === 0x1a &&
buffer[7] === 0x0a
);
}
// Get the file path for the splash image for the underlying
// system
private async getSplashPath(): Promise<string> {
// TODO: this is not perfect, if the file was supposed to be
// resin-logo.png and for some reason is not present on the folder
// the supervisor will try to write new images from the API into
// balena-logo.png to no effect. Ideally this should be configurable
// as a constant that is provided on supervisor container launch.
const [
// If no logo is found, assume the file is `balena-logo.png`
splashFile = path.join(SplashImage.BASEPATH, 'balena-logo.png'),
] = (
await Bluebird.resolve(fs.readdir(SplashImage.BASEPATH))
// Read the splash dir (will throw if the path does not exist)
// And filter valid filenames
.filter((filename) => SplashImage.FILENAMES.includes(filename))
)
// Sort by name, so in case both files are defined, balena-logo will
// be chosen
.sort()
// Convert to full path
.map((filename) => path.join(SplashImage.BASEPATH, filename));
return splashFile;
}
// Returns the base64 contents of the splash image
private async readSplashImage(where?: string): Promise<string> {
// Read from defaultPath unless where is defined
where = where ?? (await this.getSplashPath());
// read the image file...
return (await fs.readFile(where)).toString('base64');
}
// Write a splash image provided as a base64 string
private async writeSplashImage(image: string, where?: string): Promise<void> {
// Write to defaultPath unless where is defined
where = where ?? (await this.getSplashPath());
const buffer = Buffer.from(image, 'base64');
if (this.isPng(buffer)) {
// Write the buffer to the given location
await remountAndWriteAtomic(where, buffer);
} else {
throw new Error('Splash image should be a base64 encoded PNG image');
}
}
private stripPrefix(name: string): string {
if (!name.startsWith(SplashImage.PREFIX)) {
return name;
}
return name.substr(SplashImage.PREFIX.length);
}
public createConfigVarName(name: string): string {
return `${SplashImage.PREFIX}${name}`;
}
public async getBootConfig(): Promise<ConfigOptions> {
try {
const defaultImg = await this.readSplashImage(SplashImage.DEFAULT);
const img = await this.readSplashImage();
// If the image is the same as the default image
// return nothing
if (img !== defaultImg) {
return {
image: `data:image/png;base64,${img}`,
};
}
} catch (e) {
log.warn('Failed to read splash image:', e);
}
return {};
}
public async initialise(): Promise<SplashImage> {
try {
await super.initialise();
// The default boot image file has already
// been created
if (await fs.exists(SplashImage.DEFAULT)) {
return this;
}
// read the existing image file...
const image = await this.readSplashImage();
// write the image to the DEFAULT path
await this.writeSplashImage(image, SplashImage.DEFAULT);
log.success('Initialised splash image backend');
} catch (error) {
log.warn('Could not initialise splash image backend', error);
}
return this;
}
public ensureRequiredConfig(_deviceType: string, conf: ConfigOptions) {
// If the value from the cloud is empty, it is the same as no definition
if (
!_.isUndefined(conf.image) &&
_.isEmpty((conf.image as string).trim())
) {
delete conf.image;
}
return conf;
}
public isSupportedConfig(name: string): boolean {
return SplashImage.CONFIGS.includes(this.stripPrefix(name).toLowerCase());
}
public isBootConfigVar(name: string): boolean {
return SplashImage.CONFIGS.includes(this.stripPrefix(name).toLowerCase());
}
public async matches(_deviceType: string): Promise<boolean> {
// all device types
return true;
}
public processConfigVarName(name: string): string {
return this.stripPrefix(name).toLowerCase();
}
public processConfigVarValue(
_name: string,
value: string,
): string | string[] {
// check data url regex
const matches = value.match(SplashImage.DATA_URI_REGEX);
if (!_.isNull(matches)) {
const [, media, data] = matches;
const [type] = media.split(';'); // discard mediatype parameters
return `data:${type};base64,${data}`;
}
if (!_.isEmpty(value.trim())) {
// Assume data is base64 encoded. If is not, setBootConfig will fail
return `data:image/png;base64,${value}`;
}
return value.trim();
}
public async setBootConfig(opts: ConfigOptions): Promise<void> {
// If no splash image is defined, revert to the default splash image
const value = !_.isEmpty(opts.image)
? (opts.image as string)
: await this.readSplashImage(SplashImage.DEFAULT);
// If it is a data URI get only the data part
const [, image] = value.startsWith('data:') ? value.split(',') : [, value];
// Rewrite the splash image
await this.writeSplashImage(image);
}
}

View File

@ -321,9 +321,11 @@ export function formatConfigKeys(conf: {
confFromLegacyNamespace,
noNamespaceConf,
);
return _.pickBy(confWithoutNamespace, (_v, k) => {
return _.includes(validKeys, k) || matchesAnyBootConfig(k);
});
return _.pickBy(
confWithoutNamespace,
(_v, k) => _.includes(validKeys, k) || matchesAnyBootConfig(k),
);
}
export function getDefaults() {

View File

@ -1,18 +1,28 @@
import * as Bluebird from 'bluebird';
import * as _ from 'lodash';
import { fs } from 'mz';
import * as Path from 'path';
import * as constants from './constants';
import { ENOENT } from './errors';
export function writeAndSyncFile(path: string, data: string): Bluebird<void> {
export function writeAndSyncFile(
path: string,
data: string | Buffer,
): Bluebird<void> {
return Bluebird.resolve(fs.open(path, 'w')).then((fd) => {
fs.write(fd, data, 0, 'utf8')
.then(() => fs.fsync(fd))
.then(() => fs.close(fd));
_.isString(data)
? fs.write(fd, data, 0, 'utf8')
: fs
.write(fd, data, 0, data.length)
.then(() => fs.fsync(fd))
.then(() => fs.close(fd));
});
}
export function writeFileAtomic(path: string, data: string): Bluebird<void> {
export function writeFileAtomic(
path: string,
data: string | Buffer,
): Bluebird<void> {
return Bluebird.resolve(writeAndSyncFile(`${path}.new`, data)).then(() =>
fs.rename(`${path}.new`, path),
);

View File

@ -10,6 +10,7 @@ import { Extlinux } from '../src/config/backends/extlinux';
import { ConfigTxt } from '../src/config/backends/config-txt';
import { Odmdata } from '../src/config/backends/odmdata';
import { ConfigFs } from '../src/config/backends/config-fs';
import { SplashImage } from '../src/config/backends/splash-image';
import * as constants from '../src/lib/constants';
import * as config from '../src/config';
@ -19,6 +20,7 @@ const extlinuxBackend = new Extlinux();
const configTxtBackend = new ConfigTxt();
const odmdataBackend = new Odmdata();
const configFsBackend = new ConfigFs();
const splashImageBackend = new SplashImage();
describe('Device Backend Config', () => {
let logSpy: SinonSpy;
@ -524,4 +526,151 @@ describe('Device Backend Config', () => {
).to.equal(false);
});
});
describe('Boot splash image', () => {
const defaultLogo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
const png =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
const uri = `data:image/png;base64,${png}`;
beforeEach(() => {
// Setup stubs
stub(fsUtils, 'writeFileAtomic').resolves();
stub(child_process, 'exec').resolves();
});
afterEach(() => {
// Restore stubs
(fsUtils.writeFileAtomic as SinonStub).restore();
(child_process.exec as SinonStub).restore();
});
it('should correctly write to resin-logo.png', async () => {
// Devices with balenaOS < 2.51 use resin-logo.png
stub(fs, 'readdir').resolves(['resin-logo.png']);
const current = {};
const target = {
HOST_SPLASH_image: png,
};
// This should work with every device type, but testing on a couple
// of options
expect(
deviceConfig.bootConfigChangeRequired(
splashImageBackend,
current,
target,
'fincm3',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(child_process.exec).to.be.calledOnce;
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
);
// restore the stub
(fs.readdir as SinonStub).restore();
});
it('should correctly write to balena-logo.png', async () => {
// Devices with balenaOS >= 2.51 use balena-logo.png
stub(fs, 'readdir').resolves(['balena-logo.png']);
const current = {};
const target = {
HOST_SPLASH_image: png,
};
// This should work with every device type, but testing on a couple
// of options
expect(
deviceConfig.bootConfigChangeRequired(
splashImageBackend,
current,
target,
'raspberrypi4-64',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(child_process.exec).to.be.calledOnce;
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// restore the stub
(fs.readdir as SinonStub).restore();
});
it('should correctly write to balena-logo.png if no default logo is found', async () => {
// Devices with balenaOS >= 2.51 use balena-logo.png
stub(fs, 'readdir').resolves([]);
const current = {};
const target = {
HOST_SPLASH_image: png,
};
// This should work with every device type, but testing on a couple
// of options
expect(
deviceConfig.bootConfigChangeRequired(
splashImageBackend,
current,
target,
'raspberrypi3',
),
).to.equal(true);
await deviceConfig.setBootConfig(splashImageBackend, target);
expect(child_process.exec).to.be.calledOnce;
expect(logSpy).to.be.calledTwice;
expect(logSpy.getCall(1).args[2]).to.equal('Apply boot config success');
expect(fsUtils.writeFileAtomic).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// restore the stub
(fs.readdir as SinonStub).restore();
});
it('should correctly read the splash logo if different from the default', async () => {
stub(fs, 'readdir').resolves(['balena-logo.png']);
const readFileStub: SinonStub = stub(fs, 'readFile').resolves(
Buffer.from(png, 'base64') as any,
);
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(Buffer.from(defaultLogo, 'base64') as any);
expect(
await deviceConfig.getBootConfig(splashImageBackend),
).to.deep.equal({
HOST_SPLASH_image: uri,
});
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// Restore stubs
(fs.readdir as SinonStub).restore();
(fs.readFile as SinonStub).restore();
readFileStub.restore();
});
});
});

View File

@ -8,6 +8,7 @@ import { ExtraUEnv } from '../src/config/backends/extra-uEnv';
import { Extlinux } from '../src/config/backends/extlinux';
import { ConfigTxt } from '../src/config/backends/config-txt';
import { ConfigFs } from '../src/config/backends/config-fs';
import { SplashImage } from '../src/config/backends/splash-image';
import { ConfigBackend } from '../src/config/backends/backend';
describe('Config Utilities', () => {
@ -16,8 +17,9 @@ describe('Config Utilities', () => {
const configStub = stub(config, 'get').resolves('raspberry');
// Get list of backends
const devices = await configUtils.getSupportedBackends();
expect(devices.length).to.equal(1);
expect(devices.length).to.equal(2);
expect(devices[0].constructor.name).to.equal('ConfigTxt');
expect(devices[1].constructor.name).to.equal('SplashImage');
// Restore stub
configStub.restore();
// TO-DO: When we have a device that will match for multiple backends
@ -46,6 +48,7 @@ const BACKENDS: Record<string, ConfigBackend> = {
extlinux: new Extlinux(),
configtxt: new ConfigTxt(),
configfs: new ConfigFs(),
splashImage: new SplashImage(),
};
const CONFIGS = {
@ -88,6 +91,14 @@ const CONFIGS = {
foobar: 'baz',
},
},
splashImage: {
envVars: {
HOST_SPLASH_image: 'data:image/png;base64,aaa',
},
bootConfig: {
image: 'data:image/png;base64,aaa',
},
},
// TO-DO: Config-FS is commented out because it behaves differently and doesn't
// add value to the Config Utilities if we make it work but would like to add it
// configfs: {

View File

@ -0,0 +1,306 @@
import { fs, child_process } from 'mz';
import { SinonStub, stub } from 'sinon';
import { expect } from './lib/chai-config';
import * as fsUtils from '../src/lib/fs-utils';
import { SplashImage } from '../src/config/backends/splash-image';
import log from '../src/lib/supervisor-console';
describe('Splash image configuration', () => {
const backend = new SplashImage();
const defaultLogo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/wQDLA+84AAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
const logo =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=';
const uri = `data:image/png;base64,${logo}`;
let readDirStub: SinonStub;
let readFileStub: SinonStub;
let writeFileAtomicStub: SinonStub;
beforeEach(() => {
// Setup stubs
writeFileAtomicStub = stub(fsUtils, 'writeFileAtomic').resolves();
stub(child_process, 'exec').resolves();
readFileStub = stub(fs, 'readFile').resolves(
Buffer.from(logo, 'base64') as any,
);
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(Buffer.from(defaultLogo, 'base64') as any);
readDirStub = stub(fs, 'readdir').resolves(['balena-logo.png']);
});
afterEach(() => {
// Restore stubs
writeFileAtomicStub.restore();
(child_process.exec as SinonStub).restore();
readFileStub.restore();
readDirStub.restore();
});
describe('initialise', () => {
it('should make a copy of the existing boot image on initialise if not yet created', async () => {
stub(fs, 'exists').resolves(false);
// Do the initialization
await backend.initialise();
expect(fs.readFile).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
// Should make a copy
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
Buffer.from(logo, 'base64'),
);
(fs.exists as SinonStub).restore();
});
it('should skip initialization if the default image already exists', async () => {
stub(fs, 'exists').resolves(true);
// Do the initialization
await backend.initialise();
expect(fs.exists).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(fs.readFile).to.not.have.been.called;
(fs.exists as SinonStub).restore();
});
it('should fail initialization if there is no default image on the device', async () => {
stub(fs, 'exists').resolves(false);
readDirStub.resolves([]);
readFileStub.rejects();
stub(log, 'warn');
// Do the initialization
await backend.initialise();
expect(readDirStub).to.be.calledOnce;
expect(fs.readFile).to.have.been.calledOnce;
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
});
describe('getBootConfig', () => {
it('should return an empty object if the current logo matches the default logo', async () => {
readDirStub.resolves(['resin-logo.png']);
// The default logo resolves to the same value as the current logo
readFileStub
.withArgs('test/data/mnt/boot/splash/balena-logo-default.png')
.resolves(logo);
expect(await backend.getBootConfig()).to.deep.equal({});
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledTwice;
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/resin-logo.png',
);
});
it('should read the splash image from resin-logo.png if available', async () => {
readDirStub.resolves(['resin-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledTwice;
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/resin-logo.png',
);
});
it('should read the splash image from balena-logo.png if available', async () => {
readDirStub.resolves(['balena-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledTwice;
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
});
it('should read the splash image from balena-logo.png even if resin-logo.png exists', async () => {
readDirStub.resolves(['balena-logo.png', 'resin-logo.png']);
expect(await backend.getBootConfig()).to.deep.equal({
image: uri,
});
expect(readFileStub).to.be.calledTwice;
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(readFileStub).to.be.calledWith(
'test/data/mnt/boot/splash/balena-logo.png',
);
});
it('should catch readDir errors', async () => {
stub(log, 'warn');
readDirStub.rejects();
expect(await backend.getBootConfig()).to.deep.equal({});
expect(readDirStub).to.be.calledOnce;
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
it('should catch readFile errors', async () => {
stub(log, 'warn');
readDirStub.resolves([]);
readFileStub.rejects();
expect(await backend.getBootConfig()).to.deep.equal({});
expect(log.warn).to.be.calledOnce;
(log.warn as SinonStub).restore();
});
});
describe('setBootConfig', () => {
it('should write the given image to resin-logo.png if set', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({ image: uri });
expect(readDirStub).to.be.calledOnce;
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should write the given image to balena-logo.png if set', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: uri });
expect(readDirStub).to.be.calledOnce;
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should write the given image to balena-logo.png by default', async () => {
readDirStub.resolves([]);
await backend.setBootConfig({ image: uri });
expect(readDirStub).to.be.calledOnce;
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should accept just a base64 as an image', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: logo });
expect(readDirStub).to.be.calledOnce;
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(logo, 'base64'),
);
});
it('should restore balena-logo.png if image arg is unset', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({});
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore resin-logo.png if image arg is unset', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({});
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore balena-logo.png if image arg is empty', async () => {
readDirStub.resolves(['balena-logo.png']);
await backend.setBootConfig({ image: '' });
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should restore resin-logo.png if image arg is empty', async () => {
readDirStub.resolves(['resin-logo.png']);
await backend.setBootConfig({ image: '' });
expect(readDirStub).to.be.calledOnce;
expect(readFileStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/balena-logo-default.png',
);
expect(writeFileAtomicStub).to.be.calledOnceWith(
'test/data/mnt/boot/splash/resin-logo.png',
Buffer.from(defaultLogo, 'base64'),
);
});
it('should throw if arg is not a valid base64 string', async () => {
expect(backend.setBootConfig({ image: 'somestring' })).to.be.rejected;
expect(writeFileAtomicStub).to.not.be.called;
});
it('should throw if image is not a valid PNG file', async () => {
expect(backend.setBootConfig({ image: 'aGVsbG8=' })).to.be.rejected;
expect(writeFileAtomicStub).to.not.be.called;
});
});
describe('isBootConfigVar', () => {
it('Accepts any case variable names', () => {
expect(backend.isBootConfigVar('HOST_SPLASH_IMAGE')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_Image')).to.be.true;
expect(backend.isBootConfigVar('HOST_SPLASH_ImAgE')).to.be.true;
});
});
});