diff --git a/tests/commands/os/configure.spec.ts b/tests/commands/os/configure.spec.ts index e3bc3bd4..2b3eb205 100644 --- a/tests/commands/os/configure.spec.ts +++ b/tests/commands/os/configure.spec.ts @@ -21,6 +21,7 @@ import * as process from 'process'; import { runCommand } from '../../helpers'; import { promisify } from 'util'; import * as tmp from 'tmp'; +import type * as $imagefs from 'balena-image-fs'; tmp.setGracefulCleanup(); const tmpNameAsync = promisify(tmp.tmpName); @@ -29,30 +30,51 @@ import { BalenaAPIMock } from '../../nock/balena-api-mock'; if (process.platform !== 'win32') { describe('balena os configure', function () { + let imagefs: typeof $imagefs; let api: BalenaAPIMock; - let tmpPath: string; + let tmpDummyPath: string; + let tmpMatchingDtJsonPartitionPath: string; - beforeEach(async () => { + before(async function () { + // We conditionally import balena-image-fs, since when imported on top level then unrelated tests on win32 failed with: + // EPERM: operation not permitted, rename 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>.inprogress' -> 'C:\Users\RUNNER~1\AppData\Local\Temp\tmp-<...>' + // at async Object.rename (node:internal/fs/promises:782:10) { + imagefs = await import('balena-image-fs'); + tmpDummyPath = (await tmpNameAsync()) as string; + await fs.copyFile('./tests/test-data/dummy.img', tmpDummyPath); + tmpMatchingDtJsonPartitionPath = (await tmpNameAsync()) as string; + await fs.copyFile( + './tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img', + tmpMatchingDtJsonPartitionPath, + ); + }); + + beforeEach(() => { api = new BalenaAPIMock(); api.expectGetWhoAmI({ optional: true, persist: true }); - tmpPath = (await tmpNameAsync()) as string; - await fs.copyFile('./tests/test-data/dummy.img', tmpPath); }); - afterEach(async () => { + afterEach(() => { api.done(); - await fs.unlink(tmpPath); }); - it('should inject a valid config.json file', async () => { + after(async () => { + await fs.unlink(tmpDummyPath); + await fs.unlink(tmpMatchingDtJsonPartitionPath); + }); + + it('should inject a valid config.json file to an image with partition 12 as boot & matching device-type.json ', async () => { api.expectGetApplication(); + api.expectGetDeviceTypes(); + // TODO: this shouldn't be necessary & the CLI should be able to find + // everything required from the device-type.json in the image. api.expectGetConfigDeviceTypes(); api.expectDownloadConfig(); const command: string[] = [ - `os configure ${tmpPath}`, - '--device-type raspberrypi3', - '--version 2.47.0+rev1', + `os configure ${tmpMatchingDtJsonPartitionPath}`, + '--device-type jetson-nano', + '--version 6.0.13', '--fleet testApp', '--config-app-update-poll-interval 10', '--config-network ethernet', @@ -65,8 +87,57 @@ if (process.platform !== 'win32') { expect(err.join('')).to.equal(''); // confirm the image contains a config.json... - const imagefs = await import('balena-image-fs'); - const config = await imagefs.interact(tmpPath, 1, async (_fs) => { + const config = await imagefs.interact( + tmpMatchingDtJsonPartitionPath, + 12, + async (_fs) => { + const readFileAsync = promisify(_fs.readFile); + const dtJson = JSON.parse( + await readFileAsync('/device-type.json', { encoding: 'utf8' }), + ); + // confirm that the device-type.json mentions the expected partition + expect(dtJson).to.have.nested.property( + 'configuration.config.partition', + 12, + ); + return await readFileAsync('/config.json'); + }, + ); + expect(config).to.not.be.empty; + + // confirm the image has the correct config.json values... + const configObj = JSON.parse(config.toString('utf8')); + expect(configObj).to.have.property('deviceType', 'jetson-nano'); + expect(configObj).to.have.property('initialDeviceName', 'testDeviceName'); + }); + + // TODO: In the next major consider just failing when we can't find a device-types.json in the image. + it('should inject a valid config.json file to a dummy image', async () => { + api.expectGetApplication(); + // Since the dummy image doesn't include a device-type.json + // we have to reach to the API to fetch the manifest of the device type. + api.expectGetConfigDeviceTypes(); + api.expectDownloadConfig(); + + const command: string[] = [ + `os configure ${tmpDummyPath}`, + '--device-type raspberrypi3', + '--version 2.47.0+rev1', + '--fleet testApp', + '--config-app-update-poll-interval 10', + '--config-network ethernet', + '--initial-device-name testDeviceName', + '--provisioning-key-name testKey', + '--provisioning-key-expiry-date 2050-12-12', + ]; + + const { err } = await runCommand(command.join(' ')); + // Once we replace the dummy.img with one that includes a os-release & device-type.json + // then we should be able to change this to expect no errors. + expect(err.join('')).to.equal(''); + + // confirm the image contains a config.json... + const config = await imagefs.interact(tmpDummyPath, 1, async (_fs) => { return await promisify(_fs.readFile)('/config.json'); }); expect(config).to.not.be.empty; diff --git a/tests/nock/balena-api-mock.ts b/tests/nock/balena-api-mock.ts index 42ea92cb..89452f45 100644 --- a/tests/nock/balena-api-mock.ts +++ b/tests/nock/balena-api-mock.ts @@ -61,22 +61,25 @@ export class BalenaAPIMock extends NockMock { } public expectDownloadConfig(opts: ScopeOpts = {}) { - this.optPost('/download-config', opts).reply( - 200, - JSON.parse(`{ - "applicationId":1301645, - "deviceType":"raspberrypi3", - "userId":43699, - "appUpdatePollInterval":600000, - "listenPort":48484, - "vpnPort":443, - "apiEndpoint":"https://api.balena-cloud.com", - "vpnEndpoint":"vpn.balena-cloud.com", - "registryEndpoint":"registry2.balena-cloud.com", - "deltaEndpoint":"https://delta.balena-cloud.com", - "apiKey":"nothingtoseehere" - }`), - ); + this.optPost('/download-config', opts).reply(200, (_uri, body) => { + let deviceType = 'raspberrypi3'; + if (typeof body === 'object' && 'deviceType' in body) { + deviceType = body.deviceType; + } + return JSON.parse(`{ + "applicationId":1301645, + "deviceType":"${deviceType}", + "userId":43699, + "appUpdatePollInterval":600000, + "listenPort":48484, + "vpnPort":443, + "apiEndpoint":"https://api.balena-cloud.com", + "vpnEndpoint":"vpn.balena-cloud.com", + "registryEndpoint":"registry2.balena-cloud.com", + "deltaEndpoint":"https://delta.balena-cloud.com", + "apiKey":"nothingtoseehere" + }`); + }); } public expectApplicationProvisioning(opts: ScopeOpts = {}) { diff --git a/tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img b/tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img new file mode 100644 index 00000000..35eb30d3 Binary files /dev/null and b/tests/test-data/mock-jetson-nano-6.0.13.with-boot-partition-12.img differ