From 9af9b55d39142f6350212b31c04ec96d79b6e3a4 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Sun, 13 Dec 2020 15:41:59 +0100 Subject: [PATCH] Almost have randomized track tests working. --- .vscode/launch.json | 23 + server/db/Album.ts | 38 +- server/db/Artist.ts | 30 +- server/db/Data.ts | 17 +- server/db/Integration.ts | 22 +- server/db/Tag.ts | 37 +- server/db/Track.ts | 33 +- server/db/common.ts | 10 + server/endpoints/types.ts | 6 + server/lib/filterInPlace.ts | 12 + server/package-lock.json | 520 +++++++++++++++++- server/package.json | 7 + server/test/integration/flows/AlbumFlow.js | 103 ---- server/test/integration/flows/ArtistFlow.js | 102 ---- server/test/integration/flows/AuthFlow.js | 145 ----- server/test/integration/flows/AuthFlow.ts | 145 +++++ .../test/integration/flows/IntegrationFlow.js | 127 ----- server/test/integration/flows/QueryFlow.js | 384 ------------- server/test/integration/flows/ResourceFlow.ts | 203 +++++++ server/test/integration/flows/SongFlow.js | 131 ----- server/test/integration/flows/TagFlow.js | 87 --- .../{flows/helpers.js => helpers.ts} | 223 +++++--- server/test/integration/sampleDB.ts | 127 +++++ server/test/jasmine.json | 2 +- .../test/reference_model/DBReferenceModel.ts | 116 ++++ server/test/reference_model/randomGen.ts | 238 ++++++++ 26 files changed, 1584 insertions(+), 1304 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 server/db/common.ts create mode 100644 server/lib/filterInPlace.ts delete mode 100644 server/test/integration/flows/AlbumFlow.js delete mode 100644 server/test/integration/flows/ArtistFlow.js delete mode 100644 server/test/integration/flows/AuthFlow.js create mode 100644 server/test/integration/flows/AuthFlow.ts delete mode 100644 server/test/integration/flows/IntegrationFlow.js delete mode 100644 server/test/integration/flows/QueryFlow.js create mode 100644 server/test/integration/flows/ResourceFlow.ts delete mode 100644 server/test/integration/flows/SongFlow.js delete mode 100644 server/test/integration/flows/TagFlow.js rename server/test/integration/{flows/helpers.js => helpers.ts} (61%) create mode 100644 server/test/integration/sampleDB.ts create mode 100644 server/test/reference_model/DBReferenceModel.ts create mode 100644 server/test/reference_model/randomGen.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3238b8c --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/server/db/Album.ts b/server/db/Album.ts index 164bb3b..fa075ba 100644 --- a/server/db/Album.ts +++ b/server/db/Album.ts @@ -3,11 +3,14 @@ import { AlbumBaseWithRefs, AlbumWithDetails, AlbumWithRefs } from "../../client import * as api from '../../client/src/api/api'; import asJson from "../lib/asJson"; import { DBError, DBErrorKind } from "../endpoints/types"; +import { makeNotFoundError } from "./common"; var _ = require('lodash'); // Returns an album with details, or null if not found. export async function getAlbum(id: number, userId: number, knex: Knex): Promise { + + // Start transfers for tracks, tags and artists. // Also request the album itself. const tagsPromise: Promise = @@ -65,19 +68,12 @@ export async function getAlbum(id: number, userId: number, knex: Knex): }; } - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Returns the id of the created album. export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise { - return await knex.transaction(async (trx) => { - console.log("create album", album); - + return await knex.transaction(async (trx) => { // Start retrieving artists. const artistIdsPromise: Promise = trx.select('id') @@ -105,18 +101,11 @@ export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Kn // Wait for the requests to finish. var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);; - console.log("Got refs") - // Check that we found all artists and tags we need. if ((!_.isEqual(artists.sort(), (album.artistIds || []).sort())) || (!_.isEqual(tags.sort(), (album.tagIds || []).sort())) || (!_.isEqual(tracks.sort(), (album.trackIds || []).sort()))) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Create the album. @@ -165,6 +154,7 @@ export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Kn ) } + console.log('created album', album, ', ID ', albumId); return albumId; }) } @@ -214,12 +204,7 @@ export async function modifyAlbum(userId: number, albumId: number, album: AlbumB (!tags || !_.isEqual(tags.sort(), (album.tagIds || []).sort())) || (!tracks || !_.isEqual(tracks.sort(), (album.trackIds || []).sort())) || !oldAlbum) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Modify the album. @@ -356,12 +341,7 @@ export async function deleteAlbum(userId: number, albumId: number, knex: Knex): .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmAlbumId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Start deleting artist associations with the album. diff --git a/server/db/Artist.ts b/server/db/Artist.ts index 92c95cb..99eeffc 100644 --- a/server/db/Artist.ts +++ b/server/db/Artist.ts @@ -3,6 +3,7 @@ import { ArtistBaseWithRefs, ArtistWithDetails, ArtistWithRefs } from "../../cli import * as api from '../../client/src/api/api'; import asJson from "../lib/asJson"; import { DBError, DBErrorKind } from "../endpoints/types"; +import { makeNotFoundError } from "./common"; var _ = require('lodash') // Returns an artist with details, or null if not found. @@ -65,12 +66,7 @@ export async function getArtist(id: number, userId: number, knex: Knex): }; } - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Returns the id of the created artist. @@ -107,12 +103,7 @@ export async function createArtist(userId: number, artist: ArtistWithRefs, knex: if (!_.isEqual(albums.sort(), (artist.albumIds || []).sort()) || !_.isEqual(tags.sort(), (artist.tagIds || []).sort()) || !_.isEqual(tracks.sort(), (artist.trackIds || []).sort())) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Create the artist. @@ -161,6 +152,7 @@ export async function createArtist(userId: number, artist: ArtistWithRefs, knex: ) } + console.log('created artist', artist, ', ID ', artistId); return artistId; }) } @@ -210,12 +202,7 @@ export async function modifyArtist(userId: number, artistId: number, artist: Art (!tags || !_.isEqual(tags.sort(), (artist.tagIds || []).sort())) || (!tracks || !_.isEqual(tracks.sort(), (artist.trackIds || []).sort())) || !oldArtist) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Modify the artist. @@ -344,12 +331,7 @@ export async function deleteArtist(userId: number, artistId: number, knex: Knex) .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmArtistId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Start deleting artist associations with the artist. diff --git a/server/db/Data.ts b/server/db/Data.ts index 81afec7..e375b8f 100644 --- a/server/db/Data.ts +++ b/server/db/Data.ts @@ -6,6 +6,7 @@ import { createArtist } from "./Artist"; import { createTag } from "./Tag"; import { createAlbum } from "./Album"; import { createTrack } from "./Track"; +let _ = require('lodash'); export async function exportDB(userId: number, knex: Knex): Promise { // First, retrieve all the objects without taking linking tables into account. @@ -156,21 +157,21 @@ export async function exportDB(userId: number, knex: Knex): Promise { // Store the ID mappings in this record. - let tagIdMaps: Record = {}; - let artistIdMaps: Record = {}; - let albumIdMaps: Record = {}; - let trackIdMaps: Record = {}; + let tagIdMaps: Record = {}; // Maps import ID to db ID + let artistIdMaps: Record = {}; // Maps import ID to db ID + let albumIdMaps: Record = {}; // Maps import ID to db ID + let trackIdMaps: Record = {}; // Maps import ID to db ID // Insert items one by one, remapping the IDs as we go. for(const tag of db.tags) { let _tag = { - ...tag, + ..._.omit(tag, 'id'), parentId: tag.parentId ? tagIdMaps[tag.parentId] : null, } tagIdMaps[tag.id] = await createTag(userId, _tag, knex); } for(const artist of db.artists) { artistIdMaps[artist.id] = await createArtist(userId, { - ...artist, + ..._.omit(artist, 'id'), tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]), trackIds: [], albumIds: [], @@ -178,7 +179,7 @@ export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex) } for(const album of db.albums) { albumIdMaps[album.id] = await createAlbum(userId, { - ...album, + ..._.omit(album, 'id'), tagIds: album.tagIds.map((id: number) => tagIdMaps[id]), artistIds: album.artistIds.map((id: number) => artistIdMaps[id]), trackIds: [], @@ -186,7 +187,7 @@ export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex) } for(const track of db.tracks) { trackIdMaps[track.id] = await createTrack(userId, { - ...track, + ..._.omit(track, 'id'), tagIds: track.tagIds.map((id: number) => tagIdMaps[id]), artistIds: track.artistIds.map((id: number) => artistIdMaps[id]), albumId: track.albumId ? albumIdMaps[track.albumId] : null, diff --git a/server/db/Integration.ts b/server/db/Integration.ts index f5a426d..cb19e0f 100644 --- a/server/db/Integration.ts +++ b/server/db/Integration.ts @@ -3,6 +3,7 @@ import Knex from 'knex'; import asJson from '../lib/asJson'; import { DBError, DBErrorKind } from '../endpoints/types'; import { IntegrationDataWithId, IntegrationDataWithSecret, PartialIntegrationData } from '../../client/src/api/api'; +import { makeNotFoundError } from './common'; export async function createIntegration(userId: number, integration: api.IntegrationDataWithSecret, knex: Knex): Promise { return await knex.transaction(async (trx) => { @@ -37,12 +38,7 @@ export async function getIntegration(userId: number, id: number, knex: Knex): Pr } return r; } else { - let e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: "Resource not found." - } - throw e; + throw makeNotFoundError(); } } @@ -75,12 +71,7 @@ export async function deleteIntegration(userId: number, id: number, knex: Knex) // Check that we found all objects we need. if (!integrationId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: "Resource not found." - }; - throw e; + throw makeNotFoundError(); } // Delete the integration. @@ -100,12 +91,7 @@ export async function modifyIntegration(userId: number, id: number, integration: // Check that we found all objects we need. if (!integrationId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: "Resource not found", - }; - throw e; + throw makeNotFoundError(); } // Modify the integration. diff --git a/server/db/Tag.ts b/server/db/Tag.ts index 3a8e01a..717676e 100644 --- a/server/db/Tag.ts +++ b/server/db/Tag.ts @@ -3,6 +3,7 @@ import { isConstructorDeclaration } from "typescript"; import * as api from '../../client/src/api/api'; import { TagBaseWithRefs, TagWithDetails, TagWithId, TagWithRefs, TagWithRefsWithId } from "../../client/src/api/api"; import { DBError, DBErrorKind } from "../endpoints/types"; +import { makeNotFoundError } from "./common"; export async function getTagChildrenRecursive(id: number, userId: number, trx: any): Promise { const directChildren = (await trx.select('id') @@ -36,12 +37,7 @@ export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): P // Check if the parent was found, if applicable. if (tag.parentId && maybeParent !== tag.parentId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Create the new tag. @@ -57,6 +53,7 @@ export async function createTag(userId: number, tag: TagWithRefs, knex: Knex): P .returning('id') // Needed for Postgres )[0]; + console.log('created tag', tag, ', ID ', tagId); return tagId; }) } @@ -83,12 +80,7 @@ export async function deleteTag(userId: number, tagId: number, knex: Knex) { // Check that we found all objects we need. if (!tag) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Start deleting artist associations with the tag. @@ -147,12 +139,7 @@ export async function getTag(userId: number, tagId: number, knex: Knex): Promise } return result; } else { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } } @@ -180,12 +167,7 @@ export async function modifyTag(userId: number, tagId: number, tag: TagBaseWithR // Check that we found all objects we need. if ((tag.parentId && !parent) || !dbTag) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Modify the tag. @@ -220,12 +202,7 @@ export async function mergeTag(userId: number, fromId: number, toId: number, kne // Check that we found all objects we need. if (!fromTagId || !toTagId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Assign new tag ID to any objects referencing the to-be-merged tag. diff --git a/server/db/Track.ts b/server/db/Track.ts index bee6dff..85419a6 100644 --- a/server/db/Track.ts +++ b/server/db/Track.ts @@ -3,6 +3,7 @@ import { TrackBaseWithRefs, TrackWithDetails, TrackWithRefs } from "../../client import * as api from '../../client/src/api/api'; import asJson from "../lib/asJson"; import { DBError, DBErrorKind } from "../endpoints/types"; +import { makeNotFoundError } from "./common"; var _ = require('lodash') // Returns an track with details, or null if not found. @@ -65,18 +66,14 @@ export async function getTrack(id: number, userId: number, knex: Knex): storeLinks: asJson(track['storeLinks'] || []), }; } else { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } } // Returns the id of the created track. export async function createTrack(userId: number, track: TrackWithRefs, knex: Knex): Promise { return await knex.transaction(async (trx) => { + // Start retrieving artists. const artistIdsPromise: Promise = trx.select('id') @@ -112,12 +109,7 @@ export async function createTrack(userId: number, track: TrackWithRefs, knex: Kn if (!_.isEqual((artists as number[]).sort(), track.artistIds.sort()) || (!_.isEqual((tags as number[]).sort(), track.tagIds.sort())) || (track.albumId && (album === null))) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Create the track. @@ -155,6 +147,7 @@ export async function createTrack(userId: number, track: TrackWithRefs, knex: Kn ) } + console.log('created track', track, ', ID ', trackId); return trackId; }) } @@ -194,12 +187,7 @@ export async function modifyTrack(userId: number, trackId: number, track: TrackB if ((!artists || !_.isEqual(artists.sort(), (track.artistIds || []).sort())) || (!tags || !_.isEqual(tags.sort(), (track.tagIds || []).sort())) || !oldTrack) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all to-be-linked resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Modify the track. @@ -297,18 +285,13 @@ export async function deleteTrack(userId: number, trackId: number, knex: Knex): .then((r: any) => (r && r[0]) ? r[0]['id'] : undefined); if (!confirmTrackId) { - const e: DBError = { - name: "DBError", - kind: DBErrorKind.ResourceNotFound, - message: 'Not all resources were found.', - }; - throw e; + throw makeNotFoundError(); } // Start deleting artist associations with the track. const deleteArtistsPromise: Promise = trx.delete() - .from('artists_tracks') + .from('tracks_artists') .where({ 'trackId': trackId }); // Start deleting tag associations with the track. diff --git a/server/db/common.ts b/server/db/common.ts new file mode 100644 index 0000000..da5f7f2 --- /dev/null +++ b/server/db/common.ts @@ -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; +} \ No newline at end of file diff --git a/server/endpoints/types.ts b/server/endpoints/types.ts index 01d7108..100e42c 100644 --- a/server/endpoints/types.ts +++ b/server/endpoints/types.ts @@ -36,6 +36,12 @@ export function toEndpointError(e: Error): EndpointError { message: e.message, httpStatus: 404, } + } else if (isDBError(e) && e.kind === DBErrorKind.ResourceConflict) { + return { + name: "EndpointError", + message: e.message, + httpStatus: 409, + } } return { diff --git a/server/lib/filterInPlace.ts b/server/lib/filterInPlace.ts new file mode 100644 index 0000000..072faa3 --- /dev/null +++ b/server/lib/filterInPlace.ts @@ -0,0 +1,12 @@ +export default function filterInPlace(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; +} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 7d7bc04..ff4fc0e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -58,9 +58,9 @@ } }, "@types/chai": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.12.tgz", - "integrity": "sha512-aN5IAC8QNtSUdQzxu7lGBgYAOuU1tmRU4c9dIq5OKGf/SBVjXo+ffM2wEjudAWbgpOhy60nLoAGH1xm8fpCKFQ==" + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.14.tgz", + "integrity": "sha512-G+ITQPXkwTrslfG5L/BksmbLUA0M1iybEsmCWPqzSxsRRhJZimBKJkoMi8fr/CPygPTj4zO5pJH7I2/cm9M7SQ==" }, "@types/cookiejar": { "version": "2.1.1", @@ -75,6 +75,11 @@ "@types/node": "*" } }, + "@types/mocha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-8.0.4.tgz", + "integrity": "sha512-M4BwiTJjHmLq6kjON7ZoI2JMlBvpY3BYSdiP6s/qCT3jb1s9/DeJF0JELpAxiVSIxXDzfNKe+r7yedMIoLbknQ==" + }, "@types/node": { "version": "14.6.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.6.2.tgz", @@ -89,6 +94,11 @@ "safe-buffer": "*" } }, + "@types/seedrandom": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.28.tgz", + "integrity": "sha512-SMA+fUwULwK7sd/ZJicUztiPs8F1yCPwF3O23Z9uQ32ME5Ha0NmDK9+QTsYE4O2tHXChzXomSWWeIhCnoN1LqA==" + }, "@types/superagent": { "version": "3.8.7", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-3.8.7.tgz", @@ -696,6 +706,49 @@ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -815,6 +868,32 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, "crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -841,6 +920,11 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -1002,6 +1086,14 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, "escape-goat": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", @@ -1027,6 +1119,27 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + } + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -1264,6 +1377,14 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, "findup-sync": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", @@ -1464,6 +1585,11 @@ "is-property": "^1.0.2" } }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", @@ -1643,6 +1769,11 @@ "parse-passwd": "^1.0.0" } }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -1787,6 +1918,11 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==" }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, "ip-regex": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", @@ -1824,6 +1960,11 @@ } } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1982,6 +2123,11 @@ "is-unc-path": "^1.0.0" } }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -2039,6 +2185,14 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.6.0.tgz", "integrity": "sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==" }, + "jasmine-ts": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/jasmine-ts/-/jasmine-ts-0.3.0.tgz", + "integrity": "sha512-K5joodjVOh3bnD06CNXC8P5htDq/r0Rhjv66ECOpdIGFLly8kM7V+X/GXcd9kv+xO+tIq3q9Y8B5OF6yr/iiDw==", + "requires": { + "yargs": "^8.0.2" + } + }, "js-sha512": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", @@ -2069,6 +2223,11 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "json-stringify-deterministic": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.1.tgz", + "integrity": "sha512-9Fg0OY3uyzozpvJ8TVbUk09PjzhT7O2Q5kEe30g6OrKhbA/Is92igcx0XDDX7E3yAwnIlUcYLRl+ZkVrBYVP7A==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2159,6 +2318,14 @@ "package-json": "^6.3.0" } }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -2174,6 +2341,26 @@ "resolve": "^1.1.7" } }, + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, "lodash": { "version": "4.17.20", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", @@ -2250,6 +2437,14 @@ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, "memorystore": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/memorystore/-/memorystore-1.6.4.tgz", @@ -2336,6 +2531,11 @@ "mime-db": "1.44.0" } }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -2689,6 +2889,17 @@ "abbrev": "1" } }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2722,6 +2933,14 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -2843,6 +3062,16 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -2862,6 +3091,32 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, "package-json": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", @@ -2895,6 +3150,14 @@ "path-root": "^0.1.1" } }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, "parse-passwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", @@ -2932,11 +3195,21 @@ "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", @@ -2960,6 +3233,14 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "requires": { + "pify": "^2.0.0" + } + }, "pathval": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", @@ -3041,6 +3322,11 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -3167,6 +3453,25 @@ "strip-json-comments": "~2.0.1" } }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + } + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -3281,6 +3586,16 @@ } } }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -3353,6 +3668,11 @@ "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==" + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -3447,6 +3767,19 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -3587,6 +3920,34 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -3726,6 +4087,16 @@ "ansi-regex": "^4.1.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", @@ -3845,6 +4216,24 @@ "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "requires": { + "rimraf": "^3.0.0" + }, + "dependencies": { + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + } + } + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -4137,6 +4526,15 @@ "homedir-polyfill": "^1.0.1" } }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -4160,6 +4558,11 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", @@ -4200,6 +4603,48 @@ "string-width": "^4.0.0" } }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4250,11 +4695,80 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, + "yargs": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-8.0.2.tgz", + "integrity": "sha1-YpmpBVsc78lp/355wdkY3Osiw2A=", + "requires": { + "camelcase": "^4.1.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "read-pkg-up": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "yargs-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-7.0.0.tgz", + "integrity": "sha1-jQrELxbqVd69MyyvTEA4s+P139k=", + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/server/package.json b/server/package.json index 6bd9809..0b28534 100644 --- a/server/package.json +++ b/server/package.json @@ -8,6 +8,9 @@ "test": "ts-node node_modules/jasmine/bin/jasmine --config=test/jasmine.json" }, "dependencies": { + "@types/chai": "^4.2.14", + "@types/mocha": "^8.0.4", + "@types/seedrandom": "^2.4.28", "axios": "^0.21.0", "body-parser": "^1.18.3", "chai": "^4.2.0", @@ -16,7 +19,9 @@ "express-session": "^1.17.1", "http-proxy-middleware": "^1.0.6", "jasmine": "^3.6.3", + "jasmine-ts": "^0.3.0", "js-sha512": "^0.8.0", + "json-stringify-deterministic": "^1.0.1", "knex": "^0.21.12", "memorystore": "^1.6.4", "mssql": "^6.2.3", @@ -29,7 +34,9 @@ "passport-local": "^1.0.0", "pg": "^8.5.1", "querystring": "^0.2.0", + "seedrandom": "^3.0.5", "sqlite3": "^5.0.0", + "tmp": "^0.2.1", "ts-enum-util": "^4.0.2", "ts-node": "^8.10.2", "typescript": "~3.7.2" diff --git a/server/test/integration/flows/AlbumFlow.js b/server/test/integration/flows/AlbumFlow.js deleted file mode 100644 index 719db34..0000000 --- a/server/test/integration/flows/AlbumFlow.js +++ /dev/null @@ -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(); - } - }); -}); - diff --git a/server/test/integration/flows/ArtistFlow.js b/server/test/integration/flows/ArtistFlow.js deleted file mode 100644 index 04926eb..0000000 --- a/server/test/integration/flows/ArtistFlow.js +++ /dev/null @@ -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(); - } - }); -}); - diff --git a/server/test/integration/flows/AuthFlow.js b/server/test/integration/flows/AuthFlow.js deleted file mode 100644 index 61f751d..0000000 --- a/server/test/integration/flows/AuthFlow.js +++ /dev/null @@ -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(); - } - }); -}); \ No newline at end of file diff --git a/server/test/integration/flows/AuthFlow.ts b/server/test/integration/flows/AuthFlow.ts new file mode 100644 index 0000000..5cad7ff --- /dev/null +++ b/server/test/integration/flows/AuthFlow.ts @@ -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(); +// } +// }); +// }); \ No newline at end of file diff --git a/server/test/integration/flows/IntegrationFlow.js b/server/test/integration/flows/IntegrationFlow.js deleted file mode 100644 index c32d88c..0000000 --- a/server/test/integration/flows/IntegrationFlow.js +++ /dev/null @@ -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(); - } - }); -}); \ No newline at end of file diff --git a/server/test/integration/flows/QueryFlow.js b/server/test/integration/flows/QueryFlow.js deleted file mode 100644 index 3e95dd5..0000000 --- a/server/test/integration/flows/QueryFlow.js +++ /dev/null @@ -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(); - } - }); -}); \ No newline at end of file diff --git a/server/test/integration/flows/ResourceFlow.ts b/server/test/integration/flows/ResourceFlow.ts new file mode 100644 index 0000000..6541002 --- /dev/null +++ b/server/test/integration/flows/ResourceFlow.ts @@ -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 = ''; + } + 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, + highestId: number, + }; + let trackMap: IDMap = { map: new Map(), highestId: 0 }; + let albumMap: IDMap = { map: new Map(), highestId: 0 }; + let artistMap: IDMap = { map: new Map(), highestId: 0 }; + let tagMap: IDMap = { map: new Map(), 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([[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(); + } + }); +}); \ No newline at end of file diff --git a/server/test/integration/flows/SongFlow.js b/server/test/integration/flows/SongFlow.js deleted file mode 100644 index 00b7a0b..0000000 --- a/server/test/integration/flows/SongFlow.js +++ /dev/null @@ -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(); - } - }); -}); diff --git a/server/test/integration/flows/TagFlow.js b/server/test/integration/flows/TagFlow.js deleted file mode 100644 index c0f1b29..0000000 --- a/server/test/integration/flows/TagFlow.js +++ /dev/null @@ -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(); - } - }); -}); \ No newline at end of file diff --git a/server/test/integration/flows/helpers.js b/server/test/integration/helpers.ts similarity index 61% rename from server/test/integration/flows/helpers.js rename to server/test/integration/helpers.ts index 91bdc27..5f8d234 100644 --- a/server/test/integration/flows/helpers.js +++ b/server/test/integration/helpers.ts @@ -1,6 +1,11 @@ -import { expect } from "chai"; import { sha512 } from "js-sha512"; -import { IntegrationImpl } from "../../../../client/src/api"; +import { DBDataFormat, IntegrationImpl } from "../../../client/src/api/api"; +import { ReferenceDatabase } from "../reference_model/DBReferenceModel"; + +let chai = require('chai'); +let chaiHttp = require('chai-http') +chai.use(chaiHttp); +let expect = chai.expect; export async function initTestDB() { // Allow different database configs - but fall back to SQLite in memory if necessary. @@ -17,46 +22,60 @@ export async function initTestDB() { return knex; } -export async function createSong( - req, - props = { title: "Song" }, - expectStatus = undefined, +export async function createTrack( + req: any, + props = { name: "Track" }, + expectStatus: number | undefined = undefined, expectResponse = undefined ) { - await req - .post('/song') + return await req + .post('/track') .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; }); } -export async function modifySong( - req, +export async function modifyTrack( + req: any, id = 1, - props = { name: "NewSong" }, - expectStatus = undefined, + props = { name: "NewTrack" }, + expectStatus: number | undefined = undefined, ) { await req - .put('/song/' + id) + .put('/track/' + id) .send(props) - .then((res) => { + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + return res; + }); +} + +export async function deleteTrack( + req: any, + id = 1, + expectStatus: number | undefined = undefined, +) { + return await req + .delete('/track/' + id) + .send() + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }); } -export async function checkSong( - req, - id, - expectStatus = undefined, - expectResponse = undefined, +export async function checkTrack( + req: any, + id: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req - .get('/song/' + id) - .then((res) => { + .get('/track/' + id) + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -64,15 +83,15 @@ export async function checkSong( } export async function createArtist( - req, + req: any, props = { name: "Artist" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, expectResponse = undefined ) { await req .post('/artist') .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -80,29 +99,29 @@ export async function createArtist( } export async function modifyArtist( - req, + req: any, id = 1, props = { name: "NewArtist" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, ) { await req .put('/artist/' + id) .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }); } export async function checkArtist( - req, - id, - expectStatus = undefined, - expectResponse = undefined, + req: any, + id: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req .get('/artist/' + id) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -110,15 +129,15 @@ export async function checkArtist( } export async function createTag( - req, + req: any, props = { name: "Tag" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, expectResponse = undefined ) { await req .post('/tag') .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -126,29 +145,29 @@ export async function createTag( } export async function modifyTag( - req, + req: any, id = 1, props = { name: "NewTag" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, ) { await req .put('/tag/' + id) .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }); } export async function checkTag( - req, - id, - expectStatus = undefined, - expectResponse = undefined, + req: any, + id: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req .get('/tag/' + id) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -156,15 +175,15 @@ export async function checkTag( } export async function createAlbum( - req, + req: any, props = { name: "Album" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, expectResponse = undefined ) { await req .post('/album') .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -172,29 +191,29 @@ export async function createAlbum( } export async function modifyAlbum( - req, + req: any, id = 1, props = { name: "NewAlbum" }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, ) { await req .put('/album/' + id) .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }); } export async function checkAlbum( - req, - id, - expectStatus = undefined, - expectResponse = undefined, + req: any, + id: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req .get('/album/' + id) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -202,11 +221,11 @@ export async function checkAlbum( } export async function createUser( - req, - email, - password, - expectStatus = undefined, - expectResponse = undefined, + req: any, + email: string, + password: string, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { const res = await req .post('/register') @@ -220,11 +239,11 @@ export async function createUser( } export async function login( - req, - email, - password, - expectStatus = undefined, - expectResponse = undefined, + req: any, + email: string, + password: string, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { const res = await req .post('/login?username=' + encodeURIComponent(email) + '&password=' + encodeURIComponent(password)) @@ -235,9 +254,9 @@ export async function login( } export async function logout( - req, - expectStatus = undefined, - expectResponse = undefined, + req: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { const res = await req .post('/logout') @@ -248,15 +267,15 @@ export async function logout( } export async function createIntegration( - req, + req: any, props = { name: "Integration", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, expectResponse = undefined ) { await req .post('/integration') .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -264,29 +283,29 @@ export async function createIntegration( } export async function modifyIntegration( - req, + req: any, id = 1, props = { name: "NewIntegration", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, - expectStatus = undefined, + expectStatus: number | undefined = undefined, ) { await req .put('/integration/' + id) .send(props) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }); } export async function checkIntegration( - req, - id, - expectStatus = undefined, - expectResponse = undefined, + req: any, + id: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req .get('/integration/' + id) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -294,13 +313,13 @@ export async function checkIntegration( } export async function listIntegrations( - req, - expectStatus = undefined, - expectResponse = undefined, + req: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, ) { await req .get('/integration') - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); expectResponse && expect(res.body).to.deep.equal(expectResponse); return res; @@ -308,14 +327,44 @@ export async function listIntegrations( } export async function deleteIntegration( - req, - id, - expectStatus = undefined, + req: any, + id: any, + expectStatus: number | undefined = undefined, ) { await req .delete('/integration/' + id) - .then((res) => { + .then((res: any) => { expectStatus && expect(res).to.have.status(expectStatus); return res; }) +} + +export async function getExport( + req: any, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, +) { + return await req + .get('/export') + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); + return res; + }) +} + +export async function importDB( + req: any, + db: DBDataFormat, + expectStatus: number | undefined = undefined, + expectResponse: any = undefined, +) { + return await req + .post('/import') + .send(db) + .then((res: any) => { + expectStatus && expect(res).to.have.status(expectStatus); + expectResponse && expect(res.body).to.deep.equal(expectResponse); + return res; + }) } \ No newline at end of file diff --git a/server/test/integration/sampleDB.ts b/server/test/integration/sampleDB.ts new file mode 100644 index 0000000..563fc05 --- /dev/null +++ b/server/test/integration/sampleDB.ts @@ -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; \ No newline at end of file diff --git a/server/test/jasmine.json b/server/test/jasmine.json index 2365457..26680c9 100644 --- a/server/test/jasmine.json +++ b/server/test/jasmine.json @@ -1,7 +1,7 @@ { "spec_dir": "test", "spec_files": [ - "**/*[fF]low.js" + "**/*[fF]low.[tj]s" ], "helpers": [ "helpers/**/*.js" diff --git a/server/test/reference_model/DBReferenceModel.ts b/server/test/reference_model/DBReferenceModel.ts new file mode 100644 index 0000000..703232f --- /dev/null +++ b/server/test/reference_model/DBReferenceModel.ts @@ -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 + +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); +} \ No newline at end of file diff --git a/server/test/reference_model/randomGen.ts b/server/test/reference_model/randomGen.ts new file mode 100644 index 0000000..ada98ac --- /dev/null +++ b/server/test/reference_model/randomGen.ts @@ -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 = Map; + +export interface RandomDBActionDistribution { + type: Distribution, + userId: Distribution, + createTrackParams: RandomCreateTrackDistribution, + deleteTrackParams: RandomDeleteTrackDistribution, +} + +export interface RandomCreateTrackDistribution { + linkArtists: { + numValid: Distribution, + numInvalid: Distribution, + } + linkTags: { + numValid: Distribution, + numInvalid: Distribution, + } + linkAlbum: Distribution, +} + +export interface RandomDeleteTrackDistribution { + validTrack: Distribution, +} + +export function applyDistribution( + dist: Map, + 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( + 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 + } +} \ No newline at end of file