Use actions & write tests for GET /v1/device

Signed-off-by: Christina Ying Wang <christina@balena.io>
This commit is contained in:
Christina Ying Wang 2022-12-05 13:38:45 -08:00
parent 72c683d5ff
commit 250684d651
5 changed files with 103 additions and 44 deletions

View File

@ -453,3 +453,44 @@ export const getSingleContainerApp = async (appId: number) => {
releaseId: service.releaseId,
};
};
/**
* Returns legacy device info, update status, and service status for a single-container application.
* Used by:
* - GET /v1/device
*/
export const getLegacyDeviceState = async () => {
const state = await deviceState.getLegacyState();
const stateToSend = _.pick(state.local, [
'api_port',
'ip_address',
'os_version',
'mac_address',
'supervisor_version',
'update_pending',
'update_failed',
'update_downloaded',
]) as Dictionary<any>;
if (state.local?.is_on__commit != null) {
stateToSend.commit = state.local.is_on__commit;
}
// NOTE: This only returns the status of the first service,
// even in a multi-container app. We should deprecate this endpoint
// in favor of a multi-container friendly device endpoint (which doesn't
// exist yet), and use that for cloud dashboard diagnostic queries.
const service = _.toPairs(
_.toPairs(state.local?.apps)[0]?.[1]?.services,
)[0]?.[1];
if (service != null) {
stateToSend.status = service.status;
if (stateToSend.status === 'Running') {
stateToSend.status = 'Idle';
}
stateToSend.download_progress = service.download_progress;
}
return stateToSend;
};

View File

@ -235,38 +235,11 @@ router.patch('/v1/device/host-config', async (req, res) => {
}
});
router.get('/v1/device', async (_req, res) => {
router.get('/v1/device', async (_req, res, next) => {
try {
const state = await deviceState.getLegacyState();
const stateToSend = _.pick(state.local, [
'api_port',
'ip_address',
'os_version',
'mac_address',
'supervisor_version',
'update_pending',
'update_failed',
'update_downloaded',
]) as Dictionary<unknown>;
if (state.local?.is_on__commit != null) {
stateToSend.commit = state.local.is_on__commit;
}
const service = _.toPairs(
_.toPairs(state.local?.apps)[0]?.[1]?.services,
)[0]?.[1];
if (service != null) {
stateToSend.status = service.status;
if (stateToSend.status === 'Running') {
stateToSend.status = 'Idle';
}
stateToSend.download_progress = service.download_progress;
}
res.json(stateToSend);
} catch (e: any) {
res.status(500).json({
Data: '',
Error: (e != null ? e.message : undefined) || e || 'Unknown error',
});
const state = await actions.getLegacyDeviceState();
return res.json(state);
} catch (e: unknown) {
next(e);
}
});

View File

@ -378,6 +378,39 @@ describe('manages application lifecycle', () => {
expect(body.env).to.have.property('BALENA_SERVICE_NAME', serviceNames[0]);
});
it('should return legacy information about device state', async () => {
containers = await waitForSetup(targetState);
const { body } = await request(BALENA_SUPERVISOR_ADDRESS).get(
'/v1/device',
);
expect(body).to.have.property('api_port', 48484);
// Versions match semver versioning scheme: major.minor.patch(+rev)?
expect(body)
.to.have.property('os_version')
.that.matches(/balenaOS\s[1-2]\.[0-9]{1,3}\.[0-9]{1,3}(?:\+rev[0-9])?/);
expect(body)
.to.have.property('supervisor_version')
.that.matches(/(?:[0-9]+\.?){3}/);
// Matches a space-separated string of IPv4 and/or IPv6 addresses
expect(body)
.to.have.property('ip_address')
.that.matches(
/(?:(?:(?:[0-9]{1,3}\.){3}[0-9]{1,3})|(?:(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}))\s?/,
);
// Matches a space-separated string of MAC addresses
expect(body)
.to.have.property('mac_address')
.that.matches(/(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}\s?/);
expect(body).to.have.property('update_pending').that.is.a('boolean');
expect(body).to.have.property('update_failed').that.is.a('boolean');
expect(body).to.have.property('update_downloaded').that.is.a('boolean');
// Container should be running so the overall status is Idle
expect(body).to.have.property('status', 'Idle');
expect(body).to.have.property('download_progress', null);
});
// This test should be ordered last in this `describe` block, because the test compares
// the `CreatedAt` timestamps of volumes to determine whether purge was successful. Thus,
// ordering the assertion last will ensure some time has passed between the first `CreatedAt`

View File

@ -744,4 +744,28 @@ describe('device-api/v1', () => {
.expect(503);
});
});
describe('GET /v1/device', () => {
let getLegacyDeviceStateStub: SinonStub;
beforeEach(() => {
getLegacyDeviceStateStub = stub(actions, 'getLegacyDeviceState');
});
afterEach(() => getLegacyDeviceStateStub.restore());
it('responds with 200 and legacy device state', async () => {
getLegacyDeviceStateStub.resolves({ test_state: 'Success' });
await request(api)
.get('/v1/device')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200, { test_state: 'Success' });
});
it('responds with 503 for other errors that occur during request', async () => {
getLegacyDeviceStateStub.throws(new Error());
await request(api)
.get('/v1/device')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(503);
});
});
});

View File

@ -107,18 +107,6 @@ describe('SupervisorAPI [V1 Endpoints]', () => {
loggerStub.restore();
});
describe('GET /v1/device', () => {
it('returns MAC address', async () => {
const response = await request
.get('/v1/device')
.set('Accept', 'application/json')
.set('Authorization', `Bearer ${await deviceApi.getGlobalApiKey()}`)
.expect(200);
expect(response.body).to.have.property('mac_address').that.is.not.empty;
});
});
describe('/v1/device/host-config', () => {
// Wrap GET and PATCH /v1/device/host-config tests in the same block to share
// common scoped variables, namely file paths and file content