parent
ddb8d16d13
commit
9af9b55d39
26 changed files with 1584 additions and 1304 deletions
@ -0,0 +1,23 @@ |
|||||||
|
{ |
||||||
|
// Use IntelliSense to learn about possible attributes. |
||||||
|
// Hover to view descriptions of existing attributes. |
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 |
||||||
|
"version": "0.2.0", |
||||||
|
"configurations": [ |
||||||
|
{ |
||||||
|
"type": "node", |
||||||
|
"request": "launch", |
||||||
|
"name": "Jasmine Tests with SQLite", |
||||||
|
"env": { |
||||||
|
"MUDBASE_DB_CONFIG": "{\"client\": \"sqlite3\", \"connection\": \":memory:\"}" |
||||||
|
}, |
||||||
|
"program": "${workspaceFolder}/server/node_modules/jasmine-ts/lib/index", |
||||||
|
"args": [ |
||||||
|
"--config=test/jasmine.json", |
||||||
|
], |
||||||
|
"console": "integratedTerminal", |
||||||
|
"cwd": "${workspaceFolder}/server", |
||||||
|
"internalConsoleOptions": "neverOpen" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
import { DBError, DBErrorKind } from "../endpoints/types"; |
||||||
|
|
||||||
|
export function makeNotFoundError() { |
||||||
|
const e: DBError = { |
||||||
|
name: "DBError", |
||||||
|
kind: DBErrorKind.ResourceNotFound, |
||||||
|
message: 'Not all to-be-linked resources were found.', |
||||||
|
}; |
||||||
|
return e; |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
export default function filterInPlace<T>(a: T[], condition: (value: T, index: number, array: T[]) => boolean): T[] { |
||||||
|
let i = 0, j = 0; |
||||||
|
|
||||||
|
while (i < a.length) { |
||||||
|
const val = a[i]; |
||||||
|
if (condition(val, i, a)) a[j++] = val; |
||||||
|
i++; |
||||||
|
} |
||||||
|
|
||||||
|
a.length = j; |
||||||
|
return a; |
||||||
|
} |
@ -1,103 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import { expect } from 'chai'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /album with no name', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createAlbum(req, {}, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /album with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
|
|
||||||
describe('PUT /album on nonexistent album', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.modifyAlbum(req, 1, { id: 1, name: "NewAlbumName" }, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /album with an existing album', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 }); |
|
||||||
await helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200); |
|
||||||
await helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /album with tags', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) |
|
||||||
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }) |
|
||||||
await helpers.createAlbum(req, { name: "MyAlbum", tagIds: [1, 2] }, 200, { id: 1 }) |
|
||||||
await helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [1, 2], songIds: [], artistIds: [] }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
@ -1,102 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /artist with no name', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
var req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, {}, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /artist with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
var req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); |
|
||||||
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /artist on nonexistent artist', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
var req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.modifyArtist(req, 0, { id: 0, name: "NewArtistName" }, 400) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /artist with an existing artist', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
var req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); |
|
||||||
await helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200); |
|
||||||
await helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /artist with tags', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
var req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }); |
|
||||||
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }); |
|
||||||
await helpers.createArtist(req, { name: "MyArtist", tagIds: [1, 2] }, 200, { id: 1 }); |
|
||||||
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [1, 2] }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
@ -1,145 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('Auth registration password and email constraints', () => { |
|
||||||
it('are enforced', async done => { |
|
||||||
let req = await init(); |
|
||||||
try { |
|
||||||
await helpers.createUser(req, "someone", "password1A!", 400); //no valid email
|
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char
|
|
||||||
await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter
|
|
||||||
await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number
|
|
||||||
await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password
|
|
||||||
await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email
|
|
||||||
await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short
|
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('Attempting to register an already registered user', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let req = await init(); |
|
||||||
try { |
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('Auth login access for users', () => { |
|
||||||
it('is correctly enforced', async done => { |
|
||||||
let req = await init(); |
|
||||||
try { |
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200); |
|
||||||
await helpers.login(req, "someone@email.com", "password2B!", 401); |
|
||||||
await helpers.login(req, "someoneelse@other.com", "password1A!", 401); |
|
||||||
await helpers.login(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.login(req, "someoneelse@other.com", "password2B!", 200); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('Auth access to objects', () => { |
|
||||||
it('is only possible when logged in', async done => { |
|
||||||
let req = await init(); |
|
||||||
try { |
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.login(req, "someone@email.com", "password1A!", 200); |
|
||||||
|
|
||||||
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }); |
|
||||||
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} ); |
|
||||||
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 }); |
|
||||||
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 }); |
|
||||||
|
|
||||||
await helpers.checkTag(req, 1, 200); |
|
||||||
await helpers.checkAlbum(req, 1, 200); |
|
||||||
await helpers.checkArtist(req, 1, 200); |
|
||||||
await helpers.checkSong(req, 1, 200); |
|
||||||
|
|
||||||
await helpers.logout(req, 200); |
|
||||||
|
|
||||||
await helpers.checkTag(req, 1, 401); |
|
||||||
await helpers.checkAlbum(req, 1, 401); |
|
||||||
await helpers.checkArtist(req, 1, 401); |
|
||||||
await helpers.checkSong(req, 1, 401); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('Auth access to user objects', () => { |
|
||||||
it('is restricted to each user', async done => { |
|
||||||
let req = await init(); |
|
||||||
try { |
|
||||||
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200); |
|
||||||
|
|
||||||
await helpers.login(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }); |
|
||||||
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} ); |
|
||||||
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 }); |
|
||||||
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 }); |
|
||||||
await helpers.logout(req, 200); |
|
||||||
|
|
||||||
await helpers.login(req, "someoneelse@other.com", "password2B!", 200); |
|
||||||
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }); |
|
||||||
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } ); |
|
||||||
await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 }); |
|
||||||
await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 }); |
|
||||||
await helpers.logout(req, 200); |
|
||||||
|
|
||||||
await helpers.login(req, "someone@email.com", "password1A!", 200); |
|
||||||
await helpers.checkTag(req, 2, 404); |
|
||||||
await helpers.checkAlbum(req, 2, 404); |
|
||||||
await helpers.checkArtist(req, 2, 404); |
|
||||||
await helpers.checkSong(req, 2, 404); |
|
||||||
await helpers.checkTag(req, 1, 200); |
|
||||||
await helpers.checkAlbum(req, 1, 200); |
|
||||||
await helpers.checkArtist(req, 1, 200); |
|
||||||
await helpers.checkSong(req, 1, 200); |
|
||||||
await helpers.logout(req, 200); |
|
||||||
|
|
||||||
await helpers.login(req, "someoneelse@other.com", "password2B!", 200); |
|
||||||
await helpers.checkTag(req, 1, 404); |
|
||||||
await helpers.checkAlbum(req, 1, 404); |
|
||||||
await helpers.checkArtist(req, 1, 404); |
|
||||||
await helpers.checkSong(req, 1, 404); |
|
||||||
await helpers.checkTag(req, 2, 200); |
|
||||||
await helpers.checkAlbum(req, 2, 200); |
|
||||||
await helpers.checkArtist(req, 2, 200); |
|
||||||
await helpers.checkSong(req, 2, 200); |
|
||||||
await helpers.logout(req, 200); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
@ -0,0 +1,145 @@ |
|||||||
|
const chai = require('chai'); |
||||||
|
const chaiHttp = require('chai-http'); |
||||||
|
const express = require('express'); |
||||||
|
import { SetupApp } from '../../../app'; |
||||||
|
import * as helpers from '../helpers'; |
||||||
|
|
||||||
|
async function init() { |
||||||
|
chai.use(chaiHttp); |
||||||
|
const app = express(); |
||||||
|
const knex = await helpers.initTestDB(); |
||||||
|
|
||||||
|
SetupApp(app, knex, ''); |
||||||
|
|
||||||
|
// Login as a test user.
|
||||||
|
var agent = chai.request.agent(app); |
||||||
|
return agent; |
||||||
|
} |
||||||
|
|
||||||
|
describe('Auth registration password and email constraints', () => { |
||||||
|
it('are enforced', async done => { |
||||||
|
let req = await init(); |
||||||
|
try { |
||||||
|
await helpers.createUser(req, "someone", "password1A!", 400); //no valid email
|
||||||
|
await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char
|
||||||
|
await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter
|
||||||
|
await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number
|
||||||
|
await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password
|
||||||
|
await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email
|
||||||
|
await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short
|
||||||
|
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Attempting to register an already registered user', () => { |
||||||
|
it('should fail', async done => { |
||||||
|
let req = await init(); |
||||||
|
try { |
||||||
|
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
||||||
|
await helpers.createUser(req, "someone@email.com", "password1A!", 409); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Auth login access for users', () => { |
||||||
|
it('is correctly enforced', async done => { |
||||||
|
let req = await init(); |
||||||
|
try { |
||||||
|
await helpers.createUser(req, "someone@email.com", "password1A!", 200); |
||||||
|
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200); |
||||||
|
await helpers.login(req, "someone@email.com", "password2B!", 401); |
||||||
|
await helpers.login(req, "someoneelse@other.com", "password1A!", 401); |
||||||
|
await helpers.login(req, "someone@email.com", "password1A!", 200); |
||||||
|
await helpers.login(req, "someoneelse@other.com", "password2B!", 200); |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
// describe('Auth access to objects', () => {
|
||||||
|
// it('is only possible when logged in', async done => {
|
||||||
|
// let req = await init();
|
||||||
|
// try {
|
||||||
|
// await helpers.createUser(req, "someone@email.com", "password1A!", 200);
|
||||||
|
// await helpers.login(req, "someone@email.com", "password1A!", 200);
|
||||||
|
|
||||||
|
// await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
|
||||||
|
// await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
|
||||||
|
// await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
|
||||||
|
// await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
|
||||||
|
|
||||||
|
// await helpers.checkTag(req, 1, 200);
|
||||||
|
// await helpers.checkAlbum(req, 1, 200);
|
||||||
|
// await helpers.checkArtist(req, 1, 200);
|
||||||
|
// await helpers.checkSong(req, 1, 200);
|
||||||
|
|
||||||
|
// await helpers.logout(req, 200);
|
||||||
|
|
||||||
|
// await helpers.checkTag(req, 1, 401);
|
||||||
|
// await helpers.checkAlbum(req, 1, 401);
|
||||||
|
// await helpers.checkArtist(req, 1, 401);
|
||||||
|
// await helpers.checkSong(req, 1, 401);
|
||||||
|
// } finally {
|
||||||
|
// req.close();
|
||||||
|
// done();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// describe('Auth access to user objects', () => {
|
||||||
|
// it('is restricted to each user', async done => {
|
||||||
|
// let req = await init();
|
||||||
|
// try {
|
||||||
|
// await helpers.createUser(req, "someone@email.com", "password1A!", 200);
|
||||||
|
// await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
|
||||||
|
|
||||||
|
// await helpers.login(req, "someone@email.com", "password1A!", 200);
|
||||||
|
// await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
|
||||||
|
// await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
|
||||||
|
// await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
|
||||||
|
// await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
|
||||||
|
// await helpers.logout(req, 200);
|
||||||
|
|
||||||
|
// await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
|
||||||
|
// await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 });
|
||||||
|
// await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } );
|
||||||
|
// await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 });
|
||||||
|
// await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 });
|
||||||
|
// await helpers.logout(req, 200);
|
||||||
|
|
||||||
|
// await helpers.login(req, "someone@email.com", "password1A!", 200);
|
||||||
|
// await helpers.checkTag(req, 2, 404);
|
||||||
|
// await helpers.checkAlbum(req, 2, 404);
|
||||||
|
// await helpers.checkArtist(req, 2, 404);
|
||||||
|
// await helpers.checkSong(req, 2, 404);
|
||||||
|
// await helpers.checkTag(req, 1, 200);
|
||||||
|
// await helpers.checkAlbum(req, 1, 200);
|
||||||
|
// await helpers.checkArtist(req, 1, 200);
|
||||||
|
// await helpers.checkSong(req, 1, 200);
|
||||||
|
// await helpers.logout(req, 200);
|
||||||
|
|
||||||
|
// await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
|
||||||
|
// await helpers.checkTag(req, 1, 404);
|
||||||
|
// await helpers.checkAlbum(req, 1, 404);
|
||||||
|
// await helpers.checkArtist(req, 1, 404);
|
||||||
|
// await helpers.checkSong(req, 1, 404);
|
||||||
|
// await helpers.checkTag(req, 2, 200);
|
||||||
|
// await helpers.checkAlbum(req, 2, 200);
|
||||||
|
// await helpers.checkArtist(req, 2, 200);
|
||||||
|
// await helpers.checkSong(req, 2, 200);
|
||||||
|
// await helpers.logout(req, 200);
|
||||||
|
// } finally {
|
||||||
|
// req.close();
|
||||||
|
// done();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// });
|
@ -1,127 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
import { IntegrationImpl } from '../../../../client/src/api'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /integration with missing or wrong data', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 400); |
|
||||||
await helpers.createIntegration(req, { name: "A", details: {}, secretDetails: {} }, 400); |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, secretDetails: {} }, 400); |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, }, 400); |
|
||||||
await helpers.createIntegration(req, { name: "A", type: "NonexistentType", details: {}, secretDetails: {} }, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /integration with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /integration with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
|
||||||
await helpers.modifyIntegration(req, 1, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: { secret: 'cat' }, secretDetails: {} }, 200); |
|
||||||
await helpers.checkIntegration(req, 1, 200, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: { secret: 'cat' } }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /integration with wrong data', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
|
||||||
await helpers.modifyIntegration(req, 1, { name: "B", type: "UnknownType", details: {}, secretDetails: {} }, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('DELETE /integration with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
|
||||||
await helpers.checkIntegration(req, 1, 200, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {} }) |
|
||||||
await helpers.deleteIntegration(req, 1, 200); |
|
||||||
await helpers.checkIntegration(req, 1, 404); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('GET /integration list with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 }); |
|
||||||
await helpers.createIntegration(req, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 2 }); |
|
||||||
await helpers.createIntegration(req, { name: "C", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 3 }); |
|
||||||
await helpers.listIntegrations(req, 200, [ |
|
||||||
{ id: 1, name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {} }, |
|
||||||
{ id: 2, name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: {} }, |
|
||||||
{ id: 3, name: "C", type: IntegrationImpl.SpotifyClientCredentials, details: {} }, |
|
||||||
]); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
@ -1,384 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import { expect } from 'chai'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /query with no songs', () => { |
|
||||||
it('should give empty list', async done => { |
|
||||||
let agent = await init(); |
|
||||||
try { |
|
||||||
let res = await agent |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
'query': {}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [], |
|
||||||
tags: [], |
|
||||||
artists: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
} finally { |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /query with several songs and filters', () => { |
|
||||||
it('should give all correct results', async done => { |
|
||||||
const song1 = { |
|
||||||
songId: 1, |
|
||||||
title: 'Song1', |
|
||||||
storeLinks: [ 'hello my', 'darling' ], |
|
||||||
artists: [ |
|
||||||
{ |
|
||||||
artistId: 1, |
|
||||||
name: 'Artist1', |
|
||||||
storeLinks: [], |
|
||||||
} |
|
||||||
], |
|
||||||
tags: [], |
|
||||||
albums: [] |
|
||||||
}; |
|
||||||
const song2 = { |
|
||||||
songId: 2, |
|
||||||
title: 'Song2', |
|
||||||
storeLinks: [], |
|
||||||
artists: [ |
|
||||||
{ |
|
||||||
artistId: 1, |
|
||||||
name: 'Artist1', |
|
||||||
storeLinks: [], |
|
||||||
} |
|
||||||
], |
|
||||||
tags: [], |
|
||||||
albums: [] |
|
||||||
}; |
|
||||||
const song3 = { |
|
||||||
songId: 3, |
|
||||||
title: 'Song3', |
|
||||||
storeLinks: [], |
|
||||||
artists: [ |
|
||||||
{ |
|
||||||
artistId: 2, |
|
||||||
name: 'Artist2', |
|
||||||
storeLinks: [], |
|
||||||
} |
|
||||||
], |
|
||||||
tags: [], |
|
||||||
albums: [] |
|
||||||
}; |
|
||||||
|
|
||||||
async function checkAllSongs(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": {}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1, song2, song3], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkIdIn(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": { |
|
||||||
"prop": "songId", |
|
||||||
"propOperator": "IN", |
|
||||||
"propOperand": [1, 3, 5] |
|
||||||
}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1, song3], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkIdNotIn(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": { |
|
||||||
"prop": "songId", |
|
||||||
"propOperator": "NOTIN", |
|
||||||
"propOperand": [1, 3, 5] |
|
||||||
}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song2], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkArtistIdIn(req) { |
|
||||||
console.log("HERE!") |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": { |
|
||||||
"prop": "artistId", |
|
||||||
"propOperator": "IN", |
|
||||||
"propOperand": [1] |
|
||||||
}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1, song2], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkOrRelation(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": { |
|
||||||
"childrenOperator": "OR", |
|
||||||
"children": [ |
|
||||||
{ |
|
||||||
"prop": "artistId", |
|
||||||
"propOperator": "IN", |
|
||||||
"propOperand": [2] |
|
||||||
}, |
|
||||||
{ |
|
||||||
"prop": "songId", |
|
||||||
"propOperator": "EQ", |
|
||||||
"propOperand": 1 |
|
||||||
} |
|
||||||
] |
|
||||||
}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1, song3], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkStoreLinksLike(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": { |
|
||||||
"prop": "songStoreLinks", |
|
||||||
"propOperator": "LIKE", |
|
||||||
"propOperand": 'llo m' |
|
||||||
}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'details', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkResponseTypeIds(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": {}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'ids', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: [song1.songId, song2.songId, song3.songId], |
|
||||||
artists: [], |
|
||||||
tags: [], |
|
||||||
albums: [], |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
async function checkResponseTypeCount(req) { |
|
||||||
await req |
|
||||||
.post('/query') |
|
||||||
.send({ |
|
||||||
"query": {}, |
|
||||||
'offsetsLimits': { |
|
||||||
'songOffset': 0, |
|
||||||
'songLimit': 10, |
|
||||||
}, |
|
||||||
'ordering': { |
|
||||||
'orderBy': { |
|
||||||
'type': 'name', |
|
||||||
}, |
|
||||||
'ascending': true |
|
||||||
}, |
|
||||||
'responseType': 'count', |
|
||||||
}) |
|
||||||
.then((res) => { |
|
||||||
expect(res).to.have.status(200); |
|
||||||
expect(res.body).to.deep.equal({ |
|
||||||
songs: 3, |
|
||||||
artists: 0, |
|
||||||
tags: 0, |
|
||||||
albums: 0, |
|
||||||
}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "Artist1" }, 200); |
|
||||||
await helpers.createArtist(req, { name: "Artist2" }, 200); |
|
||||||
await helpers.createSong(req, { title: "Song1", artistIds: [1], storeLinks: [ 'hello my', 'darling' ] }, 200); |
|
||||||
await helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200); |
|
||||||
await helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200); |
|
||||||
await checkAllSongs(req); |
|
||||||
await checkIdIn(req); |
|
||||||
await checkIdNotIn(req); |
|
||||||
await checkArtistIdIn(req); |
|
||||||
await checkOrRelation(req); |
|
||||||
await checkStoreLinksLike(req); |
|
||||||
await checkResponseTypeCount(req); |
|
||||||
await checkResponseTypeIds(req); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
@ -0,0 +1,203 @@ |
|||||||
|
const chai = require('chai'); |
||||||
|
const chaiHttp = require('chai-http'); |
||||||
|
const express = require('express'); |
||||||
|
import { expect } from 'chai'; |
||||||
|
import { SetupApp } from '../../../app'; |
||||||
|
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 sampleDB from '../sampleDB'; |
||||||
|
let stringify = require('json-stringify-deterministic'); |
||||||
|
|
||||||
|
let _ = require('lodash'); |
||||||
|
let tmp = require('tmp'); |
||||||
|
let fs = require('fs'); |
||||||
|
|
||||||
|
async function init() { |
||||||
|
chai.use(chaiHttp); |
||||||
|
const app = express(); |
||||||
|
const knex = await helpers.initTestDB(); |
||||||
|
|
||||||
|
SetupApp(app, knex, ''); |
||||||
|
|
||||||
|
var agent = chai.request.agent(app); |
||||||
|
return agent; |
||||||
|
} |
||||||
|
|
||||||
|
// Alters a response from a real or mock DB so that they can be deep-compared
|
||||||
|
// and only non-trivial differences trigger an error.
|
||||||
|
function normalizeResponse(response: any) { |
||||||
|
let r: any = _.cloneDeep(response); |
||||||
|
if (r && 'id' in r) { |
||||||
|
r.id = '<redacted>'; |
||||||
|
} |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
// Alters a database export / reference database model so that it can be compared
|
||||||
|
// to another so that only non-trivial differences trigger an error.
|
||||||
|
function normalizeDB(oldDb: ReferenceDatabase) { |
||||||
|
let db: ReferenceDatabase = _.cloneDeep(oldDb); |
||||||
|
|
||||||
|
|
||||||
|
// Apply a deterministic sorting.
|
||||||
|
// TODO: sorting by name is not deterministic.
|
||||||
|
for (const userId in db) { |
||||||
|
db[userId].tracks.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
||||||
|
db[userId].albums.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
||||||
|
db[userId].artists.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
||||||
|
db[userId].tags.sort((a: any, b: any) => a.name.localeCompare(b.name)) |
||||||
|
} |
||||||
|
|
||||||
|
// Re-map IDs.
|
||||||
|
interface IDMap { |
||||||
|
map: Map<number, number>, |
||||||
|
highestId: number, |
||||||
|
}; |
||||||
|
let trackMap: IDMap = { map: new Map<number, number>(), highestId: 0 }; |
||||||
|
let albumMap: IDMap = { map: new Map<number, number>(), highestId: 0 }; |
||||||
|
let artistMap: IDMap = { map: new Map<number, number>(), highestId: 0 }; |
||||||
|
let tagMap: IDMap = { map: new Map<number, number>(), highestId: 0 }; |
||||||
|
let remapId = (id: number, map: IDMap) => { |
||||||
|
if (map.map.has(id)) { return map.map.get(id) as number; } |
||||||
|
let newId: number = map.highestId + 1; |
||||||
|
map.map.set(id, newId); |
||||||
|
map.highestId = newId; |
||||||
|
return newId; |
||||||
|
} |
||||||
|
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].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); }) |
||||||
|
} |
||||||
|
for (const userId in db) { |
||||||
|
// Now remap the references.
|
||||||
|
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { |
||||||
|
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap)); |
||||||
|
x.artistIds = x.artistIds.map((id: number) => remapId(id, artistMap)); |
||||||
|
x.albumId = x.albumId ? remapId(x.albumId, albumMap) : null; |
||||||
|
}); |
||||||
|
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { |
||||||
|
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap)); |
||||||
|
x.artistIds = x.artistIds.map((id: number) => remapId(id, artistMap)); |
||||||
|
x.trackIds = x.trackIds.map((id: number) => remapId(id, trackMap)); |
||||||
|
}); |
||||||
|
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { |
||||||
|
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap)); |
||||||
|
x.albumIds = x.albumIds.map((id: number) => remapId(id, albumMap)); |
||||||
|
x.trackIds = x.trackIds.map((id: number) => remapId(id, trackMap)); |
||||||
|
}); |
||||||
|
db[userId].tags.forEach((x: TagWithRefsWithId) => { |
||||||
|
x.parentId = x.parentId ? remapId(x.parentId, tagMap) : null; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return db; |
||||||
|
} |
||||||
|
|
||||||
|
describe('Randomized model-based DB back-end tests', () => { |
||||||
|
it('all succeed', async done => { |
||||||
|
let req = await init(); |
||||||
|
let actionTrace: DBAction[] = []; |
||||||
|
|
||||||
|
let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString(); |
||||||
|
console.log(`Test random seed: '${seed}'`) |
||||||
|
|
||||||
|
try { |
||||||
|
// Create a reference DB
|
||||||
|
let refDB: ReferenceDatabase = _.cloneDeep(sampleDB); |
||||||
|
|
||||||
|
// Prime the real DB
|
||||||
|
// First, create a user and log in.
|
||||||
|
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]); |
||||||
|
|
||||||
|
// Check that we are starting from an equal situation
|
||||||
|
let refState = normalizeDB(refDB); |
||||||
|
let realState = normalizeDB({ |
||||||
|
[1]: (await helpers.getExport(req)).body, |
||||||
|
}); |
||||||
|
expect(realState).to.deep.equal(refState); |
||||||
|
|
||||||
|
// Start doing some random changes, checking the state after each step.
|
||||||
|
let rng = seedrandom(seed); |
||||||
|
let dist: RandomDBActionDistribution = { |
||||||
|
type: new Map([ |
||||||
|
[DBActionType.CreateTrack, 0.7], |
||||||
|
[DBActionType.DeleteTrack, 0.3] |
||||||
|
]), |
||||||
|
userId: new Map([[1, 1.0]]), |
||||||
|
createTrackParams: { |
||||||
|
linkAlbum: new Map<boolean | 'nonexistent', number>([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]), |
||||||
|
linkTags: { |
||||||
|
numValid: new Map([[0, 1.0]]), |
||||||
|
numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), |
||||||
|
}, |
||||||
|
linkArtists: { |
||||||
|
numValid: new Map([[0, 1.0]]), |
||||||
|
numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]), |
||||||
|
}, |
||||||
|
}, |
||||||
|
deleteTrackParams: { |
||||||
|
validTrack: new Map([[false, 0.2], [true, 0.8]]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (let i = 0; i < 30; i++) { |
||||||
|
let action = 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); |
||||||
|
|
||||||
|
// Compare the response and status.
|
||||||
|
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse)); |
||||||
|
expect(realStatus).to.equal(refStatus); |
||||||
|
|
||||||
|
// Compare the database state after the action.
|
||||||
|
let refState = normalizeDB(refDB); |
||||||
|
let realState = normalizeDB({ |
||||||
|
[1]: (await helpers.getExport(req)).body, |
||||||
|
}); |
||||||
|
expect(realState).to.deep.equal(refState); |
||||||
|
} |
||||||
|
} catch (e) { |
||||||
|
// When catching a comparison error, add and dump various states to files for debugging.
|
||||||
|
e.actionTrace = actionTrace; |
||||||
|
e.startingDB = normalizeDB(sampleDB); |
||||||
|
e.testSeed = seed; |
||||||
|
if (e.actual && e.expected) { |
||||||
|
e.actualDump = tmp.tmpNameSync(); |
||||||
|
e.expectedDump = tmp.tmpNameSync(); |
||||||
|
e.actionTraceDump = tmp.tmpNameSync(); |
||||||
|
e.startingDBDump = tmp.tmpNameSync(); |
||||||
|
fs.writeFileSync(e.actualDump, stringify(e.actual, { space: ' ' })); |
||||||
|
fs.writeFileSync(e.expectedDump, stringify(e.expected, { space: ' ' })); |
||||||
|
fs.writeFileSync(e.actionTraceDump, stringify(e.actionTrace, { space: ' ' })); |
||||||
|
fs.writeFileSync(e.startingDBDump, stringify(e.startingDB, { space: ' ' })); |
||||||
|
|
||||||
|
console.log( |
||||||
|
"A comparison error occurred. Wrote compared values to temporary files for debugging:\n" |
||||||
|
+ ` actual: ${e.actualDump}\n` |
||||||
|
+ ` expected: ${e.expectedDump}\n` |
||||||
|
+ ` DB action trace: ${e.actionTraceDump}\n` |
||||||
|
+ ` Starting DB: ${e.startingDBDump}` |
||||||
|
); |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} finally { |
||||||
|
req.close(); |
||||||
|
done(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
@ -1,131 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import { expect } from 'chai'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /song with no title', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createSong(req, {}, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with only a title', () => { |
|
||||||
it('should return the first available id', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createSong(req, { title: "MySong" }, 200, { id: 1 }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with a nonexistent artist Id', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with an existing artist Id', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }); |
|
||||||
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 200, { id: 1 }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with two existing artist Ids', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) |
|
||||||
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) |
|
||||||
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with an existent and a nonexistent artist Id', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 }) |
|
||||||
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /song with tags', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 }) |
|
||||||
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 }) |
|
||||||
await helpers.createSong(req, { title: "Song", tagIds: [1, 2] }, 200, { id: 1 }) |
|
||||||
await helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [1, 2], albumIds: [], artistIds: [] }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
@ -1,87 +0,0 @@ |
|||||||
const chai = require('chai'); |
|
||||||
const chaiHttp = require('chai-http'); |
|
||||||
const express = require('express'); |
|
||||||
import { SetupApp } from '../../../app'; |
|
||||||
import { expect } from 'chai'; |
|
||||||
import * as helpers from './helpers'; |
|
||||||
import { sha512 } from 'js-sha512'; |
|
||||||
|
|
||||||
async function init() { |
|
||||||
chai.use(chaiHttp); |
|
||||||
const app = express(); |
|
||||||
const knex = await helpers.initTestDB(); |
|
||||||
|
|
||||||
// Add test users.
|
|
||||||
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users'); |
|
||||||
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users'); |
|
||||||
|
|
||||||
SetupApp(app, knex, ''); |
|
||||||
|
|
||||||
// Login as a test user.
|
|
||||||
var agent = chai.request.agent(app); |
|
||||||
await agent |
|
||||||
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1')) |
|
||||||
.send({}); |
|
||||||
return agent; |
|
||||||
} |
|
||||||
|
|
||||||
describe('POST /tag with no name', () => { |
|
||||||
it('should fail', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, {}, 400); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /tag with a correct request', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "MyTag" }, 200, { id: 1 }); |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('POST /tag with a parent', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) |
|
||||||
await helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 }) |
|
||||||
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
describe('PUT /tag with a new parent', () => { |
|
||||||
it('should succeed', async done => { |
|
||||||
let agent = await init(); |
|
||||||
let req = agent.keepOpen(); |
|
||||||
try { |
|
||||||
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 }) |
|
||||||
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 }) |
|
||||||
await helpers.modifyTag(req, 2, { parentId: 1 }, 200) |
|
||||||
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }) |
|
||||||
} finally { |
|
||||||
req.close(); |
|
||||||
agent.close(); |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
@ -0,0 +1,127 @@ |
|||||||
|
import { ReferenceDatabase } from "../reference_model/DBReferenceModel"; |
||||||
|
|
||||||
|
export const sampleDB: ReferenceDatabase = { |
||||||
|
[1]: { |
||||||
|
tracks: [ |
||||||
|
{ |
||||||
|
mbApi_typename: "track", |
||||||
|
id: 1, |
||||||
|
name: "No One Knows", |
||||||
|
artistIds: [1], |
||||||
|
tagIds: [2], |
||||||
|
albumId: 2, |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "track", |
||||||
|
id: 2, |
||||||
|
name: "See Jam", |
||||||
|
artistIds: [3], |
||||||
|
tagIds: [3, 5], |
||||||
|
albumId: 1, |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "track", |
||||||
|
id: 3, |
||||||
|
name: "Apocalypshit", |
||||||
|
artistIds: [2], |
||||||
|
tagIds: [4], |
||||||
|
albumId: 3, |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
], |
||||||
|
albums: [ |
||||||
|
{ |
||||||
|
mbApi_typename: "album", |
||||||
|
id: 1, |
||||||
|
name: "Lithuanian Artillery", |
||||||
|
artistIds: [3], |
||||||
|
tagIds: [3, 5], |
||||||
|
trackIds: [2], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "album", |
||||||
|
id: 2, |
||||||
|
name: "Songs For The Deaf", |
||||||
|
artistIds: [1], |
||||||
|
tagIds: [2], |
||||||
|
trackIds: [1], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "album", |
||||||
|
id: 3, |
||||||
|
name: "Apocalypshit", |
||||||
|
artistIds: [2], |
||||||
|
tagIds: [4], |
||||||
|
trackIds: [3], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
], |
||||||
|
artists: [ |
||||||
|
{ |
||||||
|
mbApi_typename: "artist", |
||||||
|
id: 1, |
||||||
|
name: "Queens Of The Stone Age", |
||||||
|
tagIds: [2], |
||||||
|
trackIds: [1], |
||||||
|
albumIds: [2], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "artist", |
||||||
|
id: 2, |
||||||
|
name: "Molotov", |
||||||
|
tagIds: [4], |
||||||
|
trackIds: [3], |
||||||
|
albumIds: [3], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "artist", |
||||||
|
id: 3, |
||||||
|
name: "The Schwings Band", |
||||||
|
tagIds: [3, 5], |
||||||
|
trackIds: [2], |
||||||
|
albumIds: [1], |
||||||
|
storeLinks: [], |
||||||
|
}, |
||||||
|
], |
||||||
|
tags: [ |
||||||
|
{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
id: 1, |
||||||
|
name: "Genre", |
||||||
|
parentId: null, |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
id: 2, |
||||||
|
name: "Desert Rock", |
||||||
|
parentId: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
id: 3, |
||||||
|
name: "Swing", |
||||||
|
parentId: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
id: 4, |
||||||
|
name: "Crazy", |
||||||
|
parentId: 1, |
||||||
|
}, |
||||||
|
{ |
||||||
|
mbApi_typename: "tag", |
||||||
|
id: 5, |
||||||
|
name: "Lindy Hop", |
||||||
|
parentId: null, |
||||||
|
}, |
||||||
|
], |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export default sampleDB; |
@ -0,0 +1,116 @@ |
|||||||
|
import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostTrackRequest, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api"; |
||||||
|
import { makeNotFoundError } from "../../db/common"; |
||||||
|
import filterInPlace from "../../lib/filterInPlace"; |
||||||
|
|
||||||
|
// The mock reference database is in the same format as
|
||||||
|
// the JSON import/export format, for multiple users.
|
||||||
|
export type ReferenceDatabase = Record<number, DBDataFormat> |
||||||
|
|
||||||
|
type ObjectsType = "tracks" | "artists" | "tags" | "albums"; |
||||||
|
|
||||||
|
// Get a fresh ID for a new object.
|
||||||
|
function getNewId(db: ReferenceDatabase, objectsType: ObjectsType): number { |
||||||
|
let highest: number = 1; |
||||||
|
for (const data of Object.values(db)) { |
||||||
|
data[objectsType].forEach((obj: any) => highest = Math.max(highest, obj.id)); |
||||||
|
} |
||||||
|
return highest + 1; |
||||||
|
} |
||||||
|
|
||||||
|
// Check a (set of) IDs for presence in the objects array.
|
||||||
|
// All have to exist for it to return true.
|
||||||
|
function checkExists(objects: any[], ids: number[]) { |
||||||
|
return ids.reduce((prev: boolean, id: number) => { |
||||||
|
return prev && objects.find((object: any) => object.id === id); |
||||||
|
}, true); |
||||||
|
} |
||||||
|
|
||||||
|
// If not in the array, put the number in the array.
|
||||||
|
function ensureInSet(n: number, s: number[]) { |
||||||
|
if (!(n in s)) { s.push(n); } |
||||||
|
} |
||||||
|
|
||||||
|
// For a set of objects, ensure they point to another object.
|
||||||
|
function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType, |
||||||
|
toId: number, toObjectsType: ObjectsType, data: DBDataFormat) { |
||||||
|
if (toObjectsType === 'tracks') { |
||||||
|
fromObjects.forEach((fromId: number) => ensureInSet(toId,
|
||||||
|
(data[fromObjectsType][fromId] as AlbumWithRefsWithId | ArtistWithRefsWithId).trackIds)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new object.
|
||||||
|
export interface LinkField { field: string, otherObjectType: ObjectsType }; |
||||||
|
export function createObject( |
||||||
|
userId: number, |
||||||
|
object: any, |
||||||
|
objectType: ObjectsType, |
||||||
|
singularLinkFields: LinkField[], |
||||||
|
pluralLinkFields: LinkField[], |
||||||
|
db: ReferenceDatabase |
||||||
|
): { id: number } { |
||||||
|
// Existence checks
|
||||||
|
if (!(userId in db)) { throw makeNotFoundError() } |
||||||
|
singularLinkFields.forEach((f: LinkField) => { |
||||||
|
if (!checkExists(db[userId][f.otherObjectType], object[f.field] ? [object[f.field]] : [])) { |
||||||
|
throw makeNotFoundError(); |
||||||
|
} |
||||||
|
}); |
||||||
|
pluralLinkFields.forEach((f: LinkField) => { |
||||||
|
if (!checkExists(db[userId][f.otherObjectType], object[f.field] || [])) { |
||||||
|
throw makeNotFoundError(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Create an ID and the object
|
||||||
|
let id = getNewId(db, objectType); |
||||||
|
db[userId][objectType].push({ |
||||||
|
...object, |
||||||
|
id: id, |
||||||
|
}) |
||||||
|
|
||||||
|
// reverse links
|
||||||
|
singularLinkFields.forEach((f: LinkField) => { |
||||||
|
ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, db[userId]); |
||||||
|
}); |
||||||
|
pluralLinkFields.forEach((f: LinkField) => { |
||||||
|
ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, db[userId]); |
||||||
|
}); |
||||||
|
|
||||||
|
return { id: id }; |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new track.
|
||||||
|
export function createTrack(userId: number, track: PostTrackRequest, db: ReferenceDatabase): { id: number } { |
||||||
|
return createObject( |
||||||
|
userId, |
||||||
|
track, |
||||||
|
'tracks', |
||||||
|
[{ field: 'albumId', otherObjectType: 'albums' }], |
||||||
|
[ |
||||||
|
{ field: 'artistIds', otherObjectType: 'artists' }, |
||||||
|
{ field: 'tagIds', otherObjectType: 'tags' }, |
||||||
|
], |
||||||
|
db |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
// Delete a track.
|
||||||
|
export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): void { |
||||||
|
// Existence checks
|
||||||
|
if (!(userId in db)) { throw makeNotFoundError() } |
||||||
|
|
||||||
|
// Find the object to delete.
|
||||||
|
let idx = db[userId].tracks.findIndex((track: TrackWithRefsWithId) => track.id === id); |
||||||
|
if (idx < 0) { |
||||||
|
// Not found
|
||||||
|
throw makeNotFoundError(); |
||||||
|
} |
||||||
|
|
||||||
|
// Remove references
|
||||||
|
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); }) |
||||||
|
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); }) |
||||||
|
|
||||||
|
// Delete the object
|
||||||
|
db[userId].tracks.splice(idx, 1); |
||||||
|
} |
@ -0,0 +1,238 @@ |
|||||||
|
import { AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs } from "../../../client/src/api/api"; |
||||||
|
import { userEndpoints } from "../../endpoints/User"; |
||||||
|
import { createTrack, deleteTrack, ReferenceDatabase } from "./DBReferenceModel"; |
||||||
|
import * as helpers from '../integration/helpers'; |
||||||
|
import { DBErrorKind, isDBError } from "../../endpoints/types"; |
||||||
|
|
||||||
|
export enum DBActionType { |
||||||
|
CreateTrack = 0, |
||||||
|
DeleteTrack,
|
||||||
|
} |
||||||
|
|
||||||
|
export interface DBAction { |
||||||
|
type: DBActionType, |
||||||
|
userId: number, |
||||||
|
payload: any, |
||||||
|
} |
||||||
|
|
||||||
|
export type Distribution<T> = Map<T, number>; |
||||||
|
|
||||||
|
export interface RandomDBActionDistribution { |
||||||
|
type: Distribution<DBActionType>, |
||||||
|
userId: Distribution<number>, |
||||||
|
createTrackParams: RandomCreateTrackDistribution, |
||||||
|
deleteTrackParams: RandomDeleteTrackDistribution, |
||||||
|
} |
||||||
|
|
||||||
|
export interface RandomCreateTrackDistribution { |
||||||
|
linkArtists: { |
||||||
|
numValid: Distribution<number>, |
||||||
|
numInvalid: Distribution<number>, |
||||||
|
} |
||||||
|
linkTags: { |
||||||
|
numValid: Distribution<number>, |
||||||
|
numInvalid: Distribution<number>, |
||||||
|
} |
||||||
|
linkAlbum: Distribution<boolean | 'nonexistent'>, |
||||||
|
} |
||||||
|
|
||||||
|
export interface RandomDeleteTrackDistribution { |
||||||
|
validTrack: Distribution<boolean>, |
||||||
|
} |
||||||
|
|
||||||
|
export function applyDistribution<T>( |
||||||
|
dist: Map<T, number>, |
||||||
|
randomNumGen: any, |
||||||
|
): T { |
||||||
|
let n = randomNumGen(); |
||||||
|
let r: T | undefined = undefined; |
||||||
|
dist.forEach((value: number, key: T) => { |
||||||
|
if (r) { return; } |
||||||
|
if (n <= value) { r = key; } |
||||||
|
else { n -= value; } |
||||||
|
}) |
||||||
|
if (r === undefined) { |
||||||
|
throw new Error(`Invalid distribution: n=${n}, dist ${JSON.stringify(dist.entries())}`); |
||||||
|
} |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
export function randomString(randomNumGen: any, length: number) { |
||||||
|
let chars = 'abcdefghijklmnopqrstuvwxyz'; |
||||||
|
let retval = ''; |
||||||
|
for (let i = 0; i < length; i++) { |
||||||
|
retval += chars[Math.floor(randomNumGen() * 26)]; |
||||||
|
} |
||||||
|
return retval; |
||||||
|
} |
||||||
|
|
||||||
|
export function pickNFromArray<T>( |
||||||
|
array: T[], |
||||||
|
randomNumGen: any, |
||||||
|
N: number) |
||||||
|
: T[] { |
||||||
|
let r: T[] = []; |
||||||
|
for (let i = 0; i < N; i++) { |
||||||
|
let idx = Math.floor(randomNumGen() * array.length); |
||||||
|
r.push(array[idx]); |
||||||
|
array.splice(idx); |
||||||
|
} |
||||||
|
return r; |
||||||
|
} |
||||||
|
|
||||||
|
export function applyReferenceDBAction( |
||||||
|
action: DBAction, |
||||||
|
db: ReferenceDatabase |
||||||
|
): { |
||||||
|
response: any, |
||||||
|
status: number, |
||||||
|
} { |
||||||
|
let response: any = undefined; |
||||||
|
let status: number = 0; |
||||||
|
|
||||||
|
try { |
||||||
|
switch (action.type) { |
||||||
|
case DBActionType.CreateTrack: { |
||||||
|
response = createTrack(action.userId, action.payload, db); |
||||||
|
status = 200; |
||||||
|
break; |
||||||
|
} |
||||||
|
case DBActionType.DeleteTrack: { |
||||||
|
deleteTrack(action.userId, action.payload, db); |
||||||
|
response = {}; |
||||||
|
status = 200; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} catch(e) { |
||||||
|
if(isDBError(e)) { |
||||||
|
if(e.kind === DBErrorKind.ResourceNotFound) { |
||||||
|
status = 404; |
||||||
|
response = {}; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { response: response, status: status }; |
||||||
|
} |
||||||
|
|
||||||
|
export async function applyRealDBAction( |
||||||
|
action: DBAction, |
||||||
|
req: any, |
||||||
|
): Promise<{ |
||||||
|
response: any, |
||||||
|
status: number, |
||||||
|
}> { |
||||||
|
let response: any = undefined; |
||||||
|
let status: number = 0; |
||||||
|
|
||||||
|
switch (action.type) { |
||||||
|
case DBActionType.CreateTrack: { |
||||||
|
let res = await helpers.createTrack(req, action.payload); |
||||||
|
status = res.status; |
||||||
|
response = res.body; |
||||||
|
break; |
||||||
|
} |
||||||
|
case DBActionType.DeleteTrack: { |
||||||
|
let res = await helpers.deleteTrack(req, action.payload); |
||||||
|
status = res.status; |
||||||
|
response = res.body; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { response: response, status: status }; |
||||||
|
} |
||||||
|
|
||||||
|
export function randomDBAction( |
||||||
|
db: ReferenceDatabase, |
||||||
|
randomNumGen: any, |
||||||
|
distribution: RandomDBActionDistribution, |
||||||
|
): DBAction { |
||||||
|
let type = applyDistribution( |
||||||
|
distribution.type, |
||||||
|
randomNumGen |
||||||
|
); |
||||||
|
let userId = applyDistribution( |
||||||
|
distribution.userId, |
||||||
|
randomNumGen |
||||||
|
); |
||||||
|
|
||||||
|
switch (type) { |
||||||
|
case DBActionType.CreateTrack: { |
||||||
|
return { |
||||||
|
type: type, |
||||||
|
payload: createRandomTrack( |
||||||
|
db, |
||||||
|
userId, |
||||||
|
distribution.createTrackParams, |
||||||
|
randomNumGen |
||||||
|
), |
||||||
|
userId: userId, |
||||||
|
}; |
||||||
|
} |
||||||
|
case DBActionType.DeleteTrack: { |
||||||
|
return { |
||||||
|
type: type, |
||||||
|
payload: applyDistribution(distribution.deleteTrackParams.validTrack, randomNumGen) ? |
||||||
|
Math.floor(Math.random() * db[userId].tracks.length) + 1 : |
||||||
|
Math.floor(Math.random() * db[userId].tracks.length) + 1 + db[userId].tracks.length, |
||||||
|
userId: userId, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function createRandomTrack( |
||||||
|
db: ReferenceDatabase, |
||||||
|
userId: number, |
||||||
|
trackDist: RandomCreateTrackDistribution, |
||||||
|
randomNumGen: any, |
||||||
|
): TrackWithRefs { |
||||||
|
let allValidArtistIds: number[] = db[userId] && db[userId].artists ? |
||||||
|
db[userId].artists.map((a: ArtistWithRefsWithId) => a.id) : []; |
||||||
|
let allValidTagIds: number[] = db[userId] && db[userId].tags ? |
||||||
|
db[userId].tags.map((a: TagWithRefsWithId) => a.id) : []; |
||||||
|
let allValidAlbumIds: number[] = db[userId] && db[userId].albums ? |
||||||
|
db[userId].albums.map((a: AlbumWithRefsWithId) => a.id) : []; |
||||||
|
|
||||||
|
let artists: number[] = (() => { |
||||||
|
let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen)); |
||||||
|
let invalidArtists: number[] = []; |
||||||
|
for (let i = 0; i < applyDistribution(trackDist.linkArtists.numInvalid, randomNumGen); i++) { |
||||||
|
invalidArtists.push(Math.round(Math.random() * 100) + allValidArtistIds.length); |
||||||
|
} |
||||||
|
return [...validArtists, ...invalidArtists]; |
||||||
|
})(); |
||||||
|
|
||||||
|
let tags: number[] = (() => { |
||||||
|
let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen)); |
||||||
|
let invalidTags: number[] = []; |
||||||
|
for (let i = 0; i < applyDistribution(trackDist.linkTags.numInvalid, randomNumGen); i++) { |
||||||
|
invalidTags.push(Math.round(Math.random() * 100) + allValidTagIds.length); |
||||||
|
} |
||||||
|
return [...validTags, ...invalidTags]; |
||||||
|
})(); |
||||||
|
|
||||||
|
let maybeAlbum: number | null = (() => { |
||||||
|
let r: boolean | null | 'nonexistent' = applyDistribution(trackDist.linkAlbum, randomNumGen); |
||||||
|
let maybeValidAlbum: number | null = |
||||||
|
r === true && |
||||||
|
allValidAlbumIds.length ? |
||||||
|
pickNFromArray(allValidAlbumIds, randomNumGen, 1)[0] : |
||||||
|
null; |
||||||
|
let maybeInvalidAlbum: number | null = |
||||||
|
r === 'nonexistent' ? |
||||||
|
allValidAlbumIds.length + 1 : null; |
||||||
|
return maybeValidAlbum || maybeInvalidAlbum; |
||||||
|
})(); |
||||||
|
|
||||||
|
return { |
||||||
|
mbApi_typename: 'track', |
||||||
|
albumId: maybeAlbum, |
||||||
|
artistIds: artists, |
||||||
|
tagIds: tags, |
||||||
|
name: randomString(randomNumGen, 20), |
||||||
|
storeLinks: [], // TODO
|
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue