Ensure we get input on parsing errors

We wrap JSON and date parsing code to ensure input data is logged in case of an error.

Change-type: minor
Signed-off-by: Roman Mazur <roman@balena.io>
This commit is contained in:
Roman Mazur 2019-06-26 14:25:40 +03:00
parent 7c4d8d7653
commit 645bc6c185
No known key found for this signature in database
GPG Key ID: 9459886EFE6EE2F6
2 changed files with 80 additions and 4 deletions

View File

@ -159,6 +159,16 @@ export class LocalModeManager {
});
}
// Ensures an error is thrown id timestamp string cannot be parsed.
// Date.parse may both throw or return NaN depending on a case.
private static parseTimestamp(input: string): Date {
const ms = Date.parse(input);
if (isNaN(ms)) {
throw new Error('bad date string - got Nan parsing it');
}
return new Date(ms);
}
// Read the latest stored snapshot from the database.
public async retrieveLatestSnapshot(): Promise<EngineSnapshotRecord | null> {
const r = await this.db
@ -170,10 +180,18 @@ export class LocalModeManager {
if (!r) {
return null;
}
return new EngineSnapshotRecord(
EngineSnapshot.fromJSON(r.snapshot),
new Date(Date.parse(r.timestamp)),
);
try {
return new EngineSnapshotRecord(
EngineSnapshot.fromJSON(r.snapshot),
LocalModeManager.parseTimestamp(r.timestamp),
);
} catch (e) {
// Some parsing error happened. Ensure we add data details to the error description.
throw new Error(
`Cannot parse snapshot data ${JSON.stringify(r)}.` +
`Original message: [${e.message}].`,
);
}
}
private async removeLocalModeArtifacts(objects: EngineSnapshot) {

View File

@ -1,3 +1,4 @@
import * as assert from 'assert';
import { expect } from 'chai';
import * as Docker from 'dockerode';
import * as sinon from 'sinon';
@ -277,6 +278,24 @@ describe('LocalModeManager', () => {
expect(dockerStub.getNetwork.callCount).to.be.equal(2);
removeStubs.forEach(s => expect(s.remove.callCount).to.be.equal(2));
});
it.only('skips cleanup in case of data corruption', async () => {
const removeStubs = stubRemoveMethods(false);
await db.models('engineSnapshot').insert({
snapshot: 'bad json',
timestamp: new Date().toISOString(),
});
localMode.startLocalModeChangeHandling(false);
await localMode.switchCompletion();
expect(dockerStub.getImage.notCalled).to.be.true;
expect(dockerStub.getContainer.notCalled).to.be.true;
expect(dockerStub.getVolume.notCalled).to.be.true;
expect(dockerStub.getNetwork.notCalled).to.be.true;
removeStubs.forEach(s => expect(s.remove.notCalled).to.be.true);
});
});
});
@ -310,6 +329,45 @@ describe('LocalModeManager', () => {
await localMode.storeEngineSnapshot(recordSample);
expect(await recordsCount()).to.equal(1);
});
describe('in case of data corruption', () => {
beforeEach(async () => {
await db.models('engineSnapshot').delete();
});
it('deals with snapshot data corruption', async () => {
// Write bad data to simulate corruption.
await db.models('engineSnapshot').insert({
snapshot: 'bad json',
timestamp: new Date().toISOString(),
});
try {
await localMode.retrieveLatestSnapshot();
assert.fail('Parsing error was expected');
} catch (e) {
console.log(e.message);
expect(e.message).to.contain('bad json');
}
});
it('deals with snapshot timestamp corruption', async () => {
// Write bad data to simulate corruption.
await db.models('engineSnapshot').insert({
snapshot:
'{"containers": [], "images": [], "volumes": [], "networks": []}',
timestamp: 'bad timestamp',
});
try {
await localMode.retrieveLatestSnapshot();
assert.fail('Parsing error was expected');
} catch (e) {
console.log(e.message);
expect(e.message).to.contain('bad timestamp');
}
});
});
});
after(async () => {