|
|
|
@ -7,7 +7,7 @@ import { ReferenceDatabase } from '../../reference_model/DBReferenceModel'; |
|
|
|
|
import { randomDBAction, RandomDBActionDistribution, DBActionType, applyReferenceDBAction, applyRealDBAction, DBAction } from '../../reference_model/randomGen'; |
|
|
|
|
import * as helpers from '../helpers'; |
|
|
|
|
import seedrandom from 'seedrandom'; |
|
|
|
|
import { AlbumWithRefsWithId, Artist, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from '../../../../client/src/api/api'; |
|
|
|
|
import { AlbumWithRefsWithId, Artist, ArtistWithRefsWithId, DBImportResponse, IDMappings, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from '../../../../client/src/api/api'; |
|
|
|
|
import sampleDB from '../sampleDB'; |
|
|
|
|
let stringify = require('json-stringify-deterministic'); |
|
|
|
|
|
|
|
|
@ -69,7 +69,7 @@ function normalizeDB(oldDb: ReferenceDatabase) { |
|
|
|
|
} |
|
|
|
|
for (const userId in db) { |
|
|
|
|
// First remap the IDs only, ignoring references
|
|
|
|
|
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { console.log("X:", x); x.id = remapId(x.id, trackMap); }); |
|
|
|
|
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { x.id = remapId(x.id, trackMap); }); |
|
|
|
|
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { x.id = remapId(x.id, albumMap); }) |
|
|
|
|
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { x.id = remapId(x.id, artistMap); }) |
|
|
|
|
db[userId].tags.forEach((x: TagWithRefsWithId) => { x.id = remapId(x.id, tagMap); }) |
|
|
|
@ -99,16 +99,61 @@ function normalizeDB(oldDb: ReferenceDatabase) { |
|
|
|
|
return db; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Transform all referenced IDs in an action given a mapping.
|
|
|
|
|
function transformActionIDs(action: DBAction, mappings: IDMappings, rng: any) { |
|
|
|
|
let r: DBAction = _.cloneDeep(action); |
|
|
|
|
|
|
|
|
|
let doMap = (id: number, mapping: Record<number, number>) => { |
|
|
|
|
if (id in mapping) { |
|
|
|
|
return mapping[id]; |
|
|
|
|
} |
|
|
|
|
// Number is not in the map. Generate something that will be nonexistent
|
|
|
|
|
// for sure.
|
|
|
|
|
let highest = 0; |
|
|
|
|
for (const [_, value] of Object.entries(mapping)) { |
|
|
|
|
highest = Math.max(value, highest); |
|
|
|
|
} |
|
|
|
|
return highest + 1 + Math.floor(rng() * 100); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
switch (r.type) { |
|
|
|
|
case DBActionType.CreateTrack: { |
|
|
|
|
let track = r.payload as TrackWithRefsWithId; |
|
|
|
|
track.tagIds.forEach((id: number) => doMap(id, mappings.tags)); |
|
|
|
|
track.artistIds.forEach((id: number) => doMap(id, mappings.artists)); |
|
|
|
|
track.albumId = track.albumId ? doMap(track.albumId, mappings.albums) : null; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
case DBActionType.DeleteTrack: { |
|
|
|
|
r.payload = mappings.tracks[r.payload]; |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return r; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// This test is designed to find corner-cases in the back-end API implementation.
|
|
|
|
|
// Operations are performed on the API while also being performed on a simplified
|
|
|
|
|
// reference model.
|
|
|
|
|
// The database is compared after each operation by exporting it through the export
|
|
|
|
|
// endpoint.
|
|
|
|
|
// This way, the import, export and resource operations are all thoroughly tested
|
|
|
|
|
// in a random way, assuming that the reference model is the "gold standard".
|
|
|
|
|
describe('Randomized model-based DB back-end tests', () => { |
|
|
|
|
it('all succeed', async done => { |
|
|
|
|
// Get a handle to perform API requests.
|
|
|
|
|
let req = await init(); |
|
|
|
|
|
|
|
|
|
// Keep a trace of the actions that have been performed, for debugging purposes.
|
|
|
|
|
let actionTrace: DBAction[] = []; |
|
|
|
|
|
|
|
|
|
// Seed the random number generator so that the test run can be reproduced.
|
|
|
|
|
let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString(); |
|
|
|
|
console.log(`Test random seed: '${seed}'`) |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Create a reference DB
|
|
|
|
|
// Create a reference DB.
|
|
|
|
|
let refDB: ReferenceDatabase = _.cloneDeep(sampleDB); |
|
|
|
|
|
|
|
|
|
// Prime the real DB
|
|
|
|
@ -116,7 +161,13 @@ describe('Randomized model-based DB back-end tests', () => { |
|
|
|
|
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
|
|
|
await helpers.login(req, "someone@email.com", "password1A!", 200); |
|
|
|
|
// Import the starting DB.
|
|
|
|
|
await helpers.importDB(req, refDB[1]); |
|
|
|
|
let importResponse: DBImportResponse = (await helpers.importDB(req, refDB[1])).body; |
|
|
|
|
|
|
|
|
|
// As we perform operations, the real DB and reference model may assign
|
|
|
|
|
// different new IDs to new objects. We need to maintain mappings in order
|
|
|
|
|
// to keep the operations consistent (e.g. when we make references to)
|
|
|
|
|
// existing objects along the way.
|
|
|
|
|
let idMappingsRefToReal: IDMappings = importResponse; |
|
|
|
|
|
|
|
|
|
// Check that we are starting from an equal situation
|
|
|
|
|
let refState = normalizeDB(refDB); |
|
|
|
@ -125,8 +176,11 @@ describe('Randomized model-based DB back-end tests', () => { |
|
|
|
|
}); |
|
|
|
|
expect(realState).to.deep.equal(refState); |
|
|
|
|
|
|
|
|
|
// Start doing some random changes, checking the state after each step.
|
|
|
|
|
// Create a random number generator to use throughout the test.
|
|
|
|
|
let rng = seedrandom(seed); |
|
|
|
|
|
|
|
|
|
// This distribution determines the kind of random actions that will
|
|
|
|
|
// be generated.
|
|
|
|
|
let dist: RandomDBActionDistribution = { |
|
|
|
|
type: new Map([ |
|
|
|
|
[DBActionType.CreateTrack, 0.7], |
|
|
|
@ -149,20 +203,35 @@ describe('Randomized model-based DB back-end tests', () => { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 30; i++) { |
|
|
|
|
let action = randomDBAction( |
|
|
|
|
// Loop to generate and execute a bunch of random actions.
|
|
|
|
|
for (let i = 0; i < 100; i++) { |
|
|
|
|
|
|
|
|
|
// Generate an action (based on the reference DB)
|
|
|
|
|
let refAction = randomDBAction( |
|
|
|
|
refDB, |
|
|
|
|
rng, |
|
|
|
|
dist |
|
|
|
|
); |
|
|
|
|
actionTrace.push(action); |
|
|
|
|
console.log("Testing action: ", action); |
|
|
|
|
let { response: refResponse, status: refStatus } = applyReferenceDBAction(action, refDB); |
|
|
|
|
let { response: realResponse, status: realStatus } = await applyRealDBAction(action, req); |
|
|
|
|
actionTrace.push(refAction); |
|
|
|
|
console.log("Testing action: ", refAction); |
|
|
|
|
|
|
|
|
|
// Transform any referenced IDs using the ID mapping
|
|
|
|
|
let realAction = transformActionIDs(refAction, idMappingsRefToReal, rng); |
|
|
|
|
|
|
|
|
|
// Apply the action to the real and reference DB.
|
|
|
|
|
let { response: refResponse, status: refStatus } = applyReferenceDBAction(refAction, refDB); |
|
|
|
|
let { response: realResponse, status: realStatus } = await applyRealDBAction(realAction, req); |
|
|
|
|
|
|
|
|
|
// If this was an object creation action, we need to update the mappings.
|
|
|
|
|
if (refAction.type === DBActionType.CreateTrack) { |
|
|
|
|
let refId = refResponse.id; |
|
|
|
|
let realId = realResponse.id; |
|
|
|
|
idMappingsRefToReal.tracks[refId] = realId; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Compare the response and status.
|
|
|
|
|
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse)); |
|
|
|
|
expect(realStatus).to.equal(refStatus); |
|
|
|
|
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse)); |
|
|
|
|
|
|
|
|
|
// Compare the database state after the action.
|
|
|
|
|
let refState = normalizeDB(refDB); |
|
|
|
@ -191,7 +260,8 @@ describe('Randomized model-based DB back-end tests', () => { |
|
|
|
|
+ ` actual: ${e.actualDump}\n` |
|
|
|
|
+ ` expected: ${e.expectedDump}\n` |
|
|
|
|
+ ` DB action trace: ${e.actionTraceDump}\n` |
|
|
|
|
+ ` Starting DB: ${e.startingDBDump}` |
|
|
|
|
+ ` Starting DB: ${e.startingDBDump}\n` |
|
|
|
|
+ ` TEST_RANDOM_SEED: ${seed}` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
throw e; |
|
|
|
|