Almost have randomized track tests working.

editsong
Sander Vocke 5 years ago
parent ddb8d16d13
commit 9af9b55d39
  1. 23
      .vscode/launch.json
  2. 38
      server/db/Album.ts
  3. 30
      server/db/Artist.ts
  4. 17
      server/db/Data.ts
  5. 22
      server/db/Integration.ts
  6. 37
      server/db/Tag.ts
  7. 33
      server/db/Track.ts
  8. 10
      server/db/common.ts
  9. 6
      server/endpoints/types.ts
  10. 12
      server/lib/filterInPlace.ts
  11. 520
      server/package-lock.json
  12. 7
      server/package.json
  13. 103
      server/test/integration/flows/AlbumFlow.js
  14. 102
      server/test/integration/flows/ArtistFlow.js
  15. 145
      server/test/integration/flows/AuthFlow.js
  16. 145
      server/test/integration/flows/AuthFlow.ts
  17. 127
      server/test/integration/flows/IntegrationFlow.js
  18. 384
      server/test/integration/flows/QueryFlow.js
  19. 203
      server/test/integration/flows/ResourceFlow.ts
  20. 131
      server/test/integration/flows/SongFlow.js
  21. 87
      server/test/integration/flows/TagFlow.js
  22. 223
      server/test/integration/helpers.ts
  23. 127
      server/test/integration/sampleDB.ts
  24. 2
      server/test/jasmine.json
  25. 116
      server/test/reference_model/DBReferenceModel.ts
  26. 238
      server/test/reference_model/randomGen.ts

@ -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"
}
]
}

@ -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<AlbumWithDetails> {
// Start transfers for tracks, tags and artists.
// Also request the album itself.
const tagsPromise: Promise<api.TagWithId[]> =
@ -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<number> {
return await knex.transaction(async (trx) => {
console.log("create album", album);
return await knex.transaction(async (trx) => {
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
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.

@ -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.

@ -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<api.DBDataFormat> {
// First, retrieve all the objects without taking linking tables into account.
@ -156,21 +157,21 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFo
export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex): Promise<void> {
// Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {};
let artistIdMaps: Record<number, number> = {};
let albumIdMaps: Record<number, number> = {};
let trackIdMaps: Record<number, number> = {};
let tagIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let artistIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let albumIdMaps: Record<number, number> = {}; // Maps import ID to db ID
let trackIdMaps: Record<number, number> = {}; // 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,

@ -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<number> {
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.

@ -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<number[]> {
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.

@ -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<number> {
return await knex.transaction(async (trx) => {
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
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<any> =
trx.delete()
.from('artists_tracks')
.from('tracks_artists')
.where({ 'trackId': trackId });
// Start deleting tag associations with the track.

@ -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;
}

@ -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 {

@ -0,0 +1,12 @@
export default function filterInPlace<T>(a: T[], condition: (value: T, index: number, array: T[]) => boolean): T[] {
let i = 0, j = 0;
while (i < a.length) {
const val = a[i];
if (condition(val, i, a)) a[j++] = val;
i++;
}
a.length = j;
return a;
}

@ -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",

@ -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"

@ -1,103 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /album with no name', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /album with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /album on nonexistent album', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.modifyAlbum(req, 1, { id: 1, name: "NewAlbumName" }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /album with an existing album', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createAlbum(req, { name: "MyAlbum" }, 200, { id: 1 });
await helpers.modifyAlbum(req, 1, { name: "MyNewAlbum" }, 200);
await helpers.checkAlbum(req, 1, 200, { name: "MyNewAlbum", storeLinks: [], tagIds: [], songIds: [], artistIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /album with tags', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })
await helpers.createAlbum(req, { name: "MyAlbum", tagIds: [1, 2] }, 200, { id: 1 })
await helpers.checkAlbum(req, 1, 200, { name: "MyAlbum", storeLinks: [], tagIds: [1, 2], songIds: [], artistIds: [] })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -1,102 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /artist with no name', () => {
it('should fail', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /artist with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /artist on nonexistent artist', () => {
it('should fail', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.modifyArtist(req, 0, { id: 0, name: "NewArtistName" }, 400)
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /artist with an existing artist', () => {
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200);
await helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [], tagIds: [] });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /artist with tags', () => {
it('should succeed', async done => {
let agent = await init();
var req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 });
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 });
await helpers.createArtist(req, { name: "MyArtist", tagIds: [1, 2] }, 200, { id: 1 });
await helpers.checkArtist(req, 1, 200, { name: "MyArtist", storeLinks: [], tagIds: [1, 2] });
} finally {
req.close();
agent.close();
done();
}
});
});

@ -1,145 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
return agent;
}
describe('Auth registration password and email constraints', () => {
it('are enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone", "password1A!", 400); //no valid email
await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char
await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter
await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number
await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password
await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email
await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
} finally {
req.close();
done();
}
});
});
describe('Attempting to register an already registered user', () => {
it('should fail', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someone@email.com", "password1A!", 400);
} finally {
req.close();
done();
}
});
});
describe('Auth login access for users', () => {
it('is correctly enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
await helpers.login(req, "someone@email.com", "password2B!", 401);
await helpers.login(req, "someoneelse@other.com", "password1A!", 401);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
} finally {
req.close();
done();
}
});
});
describe('Auth access to objects', () => {
it('is only possible when logged in', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
await helpers.checkTag(req, 1, 200);
await helpers.checkAlbum(req, 1, 200);
await helpers.checkArtist(req, 1, 200);
await helpers.checkSong(req, 1, 200);
await helpers.logout(req, 200);
await helpers.checkTag(req, 1, 401);
await helpers.checkAlbum(req, 1, 401);
await helpers.checkArtist(req, 1, 401);
await helpers.checkSong(req, 1, 401);
} finally {
req.close();
done();
}
});
});
describe('Auth access to user objects', () => {
it('is restricted to each user', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
await helpers.logout(req, 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 });
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } );
await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 });
await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 });
await helpers.logout(req, 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.checkTag(req, 2, 404);
await helpers.checkAlbum(req, 2, 404);
await helpers.checkArtist(req, 2, 404);
await helpers.checkSong(req, 2, 404);
await helpers.checkTag(req, 1, 200);
await helpers.checkAlbum(req, 1, 200);
await helpers.checkArtist(req, 1, 200);
await helpers.checkSong(req, 1, 200);
await helpers.logout(req, 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
await helpers.checkTag(req, 1, 404);
await helpers.checkAlbum(req, 1, 404);
await helpers.checkArtist(req, 1, 404);
await helpers.checkSong(req, 1, 404);
await helpers.checkTag(req, 2, 200);
await helpers.checkAlbum(req, 2, 200);
await helpers.checkArtist(req, 2, 200);
await helpers.checkSong(req, 2, 200);
await helpers.logout(req, 200);
} finally {
req.close();
done();
}
});
});

@ -0,0 +1,145 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import * as helpers from '../helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
return agent;
}
describe('Auth registration password and email constraints', () => {
it('are enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone", "password1A!", 400); //no valid email
await helpers.createUser(req, "someone@email.com", "password1A", 400); //no special char
await helpers.createUser(req, "someone@email.com", "password1!", 400); //no capital letter
await helpers.createUser(req, "someone@email.com", "passwordA!", 400); //no number
await helpers.createUser(req, "someone@email.com", "Ϭassword1A!", 400); //non-ASCII in password
await helpers.createUser(req, "Ϭomeone@email.com", "password1A!", 400); //non-ASCII in email
await helpers.createUser(req, "someone@email.com", "pass1A!", 400); //password too short
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
} finally {
req.close();
done();
}
});
});
describe('Attempting to register an already registered user', () => {
it('should fail', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someone@email.com", "password1A!", 409);
} finally {
req.close();
done();
}
});
});
describe('Auth login access for users', () => {
it('is correctly enforced', async done => {
let req = await init();
try {
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
await helpers.login(req, "someone@email.com", "password2B!", 401);
await helpers.login(req, "someoneelse@other.com", "password1A!", 401);
await helpers.login(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
} finally {
req.close();
done();
}
});
});
// describe('Auth access to objects', () => {
// it('is only possible when logged in', async done => {
// let req = await init();
// try {
// await helpers.createUser(req, "someone@email.com", "password1A!", 200);
// await helpers.login(req, "someone@email.com", "password1A!", 200);
// await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
// await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
// await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
// await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
// await helpers.checkTag(req, 1, 200);
// await helpers.checkAlbum(req, 1, 200);
// await helpers.checkArtist(req, 1, 200);
// await helpers.checkSong(req, 1, 200);
// await helpers.logout(req, 200);
// await helpers.checkTag(req, 1, 401);
// await helpers.checkAlbum(req, 1, 401);
// await helpers.checkArtist(req, 1, 401);
// await helpers.checkSong(req, 1, 401);
// } finally {
// req.close();
// done();
// }
// });
// });
// describe('Auth access to user objects', () => {
// it('is restricted to each user', async done => {
// let req = await init();
// try {
// await helpers.createUser(req, "someone@email.com", "password1A!", 200);
// await helpers.createUser(req, "someoneelse@other.com", "password2B!", 200);
// await helpers.login(req, "someone@email.com", "password1A!", 200);
// await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 });
// await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1} );
// await helpers.createAlbum(req, { name: "Album1" }, 200, { id: 1 });
// await helpers.createSong(req, { title: "Song1" }, 200, { id: 1 });
// await helpers.logout(req, 200);
// await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
// await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 });
// await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 } );
// await helpers.createAlbum(req, { name: "Album2" }, 200, { id: 2 });
// await helpers.createSong(req, { title: "Song2" }, 200, { id: 2 });
// await helpers.logout(req, 200);
// await helpers.login(req, "someone@email.com", "password1A!", 200);
// await helpers.checkTag(req, 2, 404);
// await helpers.checkAlbum(req, 2, 404);
// await helpers.checkArtist(req, 2, 404);
// await helpers.checkSong(req, 2, 404);
// await helpers.checkTag(req, 1, 200);
// await helpers.checkAlbum(req, 1, 200);
// await helpers.checkArtist(req, 1, 200);
// await helpers.checkSong(req, 1, 200);
// await helpers.logout(req, 200);
// await helpers.login(req, "someoneelse@other.com", "password2B!", 200);
// await helpers.checkTag(req, 1, 404);
// await helpers.checkAlbum(req, 1, 404);
// await helpers.checkArtist(req, 1, 404);
// await helpers.checkSong(req, 1, 404);
// await helpers.checkTag(req, 2, 200);
// await helpers.checkAlbum(req, 2, 200);
// await helpers.checkArtist(req, 2, 200);
// await helpers.checkSong(req, 2, 200);
// await helpers.logout(req, 200);
// } finally {
// req.close();
// done();
// }
// });
// });

@ -1,127 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
import { IntegrationImpl } from '../../../../client/src/api';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /integration with missing or wrong data', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", details: {}, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, secretDetails: {} }, 400);
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, }, 400);
await helpers.createIntegration(req, { name: "A", type: "NonexistentType", details: {}, secretDetails: {} }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /integration with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /integration with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: { secret: 'cat' }, secretDetails: {} }, 200);
await helpers.checkIntegration(req, 1, 200, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: { secret: 'cat' } })
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /integration with wrong data', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.modifyIntegration(req, 1, { name: "B", type: "UnknownType", details: {}, secretDetails: {} }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('DELETE /integration with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.checkIntegration(req, 1, 200, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {} })
await helpers.deleteIntegration(req, 1, 200);
await helpers.checkIntegration(req, 1, 404);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('GET /integration list with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createIntegration(req, { name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 1 });
await helpers.createIntegration(req, { name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 2 });
await helpers.createIntegration(req, { name: "C", type: IntegrationImpl.SpotifyClientCredentials, details: {}, secretDetails: {} }, 200, { id: 3 });
await helpers.listIntegrations(req, 200, [
{ id: 1, name: "A", type: IntegrationImpl.SpotifyClientCredentials, details: {} },
{ id: 2, name: "B", type: IntegrationImpl.SpotifyClientCredentials, details: {} },
{ id: 3, name: "C", type: IntegrationImpl.SpotifyClientCredentials, details: {} },
]);
} finally {
req.close();
agent.close();
done();
}
});
});

@ -1,384 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /query with no songs', () => {
it('should give empty list', async done => {
let agent = await init();
try {
let res = await agent
.post('/query')
.send({
'query': {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [],
tags: [],
artists: [],
albums: [],
});
} finally {
agent.close();
done();
}
});
});
describe('POST /query with several songs and filters', () => {
it('should give all correct results', async done => {
const song1 = {
songId: 1,
title: 'Song1',
storeLinks: [ 'hello my', 'darling' ],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song2 = {
songId: 2,
title: 'Song2',
storeLinks: [],
artists: [
{
artistId: 1,
name: 'Artist1',
storeLinks: [],
}
],
tags: [],
albums: []
};
const song3 = {
songId: 3,
title: 'Song3',
storeLinks: [],
artists: [
{
artistId: 2,
name: 'Artist2',
storeLinks: [],
}
],
tags: [],
albums: []
};
async function checkAllSongs(req) {
await req
.post('/query')
.send({
"query": {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song2, song3],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkIdIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "IN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song3],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkIdNotIn(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songId",
"propOperator": "NOTIN",
"propOperand": [1, 3, 5]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song2],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkArtistIdIn(req) {
console.log("HERE!")
await req
.post('/query')
.send({
"query": {
"prop": "artistId",
"propOperator": "IN",
"propOperand": [1]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song2],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkOrRelation(req) {
await req
.post('/query')
.send({
"query": {
"childrenOperator": "OR",
"children": [
{
"prop": "artistId",
"propOperator": "IN",
"propOperand": [2]
},
{
"prop": "songId",
"propOperator": "EQ",
"propOperand": 1
}
]
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1, song3],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkStoreLinksLike(req) {
await req
.post('/query')
.send({
"query": {
"prop": "songStoreLinks",
"propOperator": "LIKE",
"propOperand": 'llo m'
},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'details',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkResponseTypeIds(req) {
await req
.post('/query')
.send({
"query": {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'ids',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: [song1.songId, song2.songId, song3.songId],
artists: [],
tags: [],
albums: [],
});
});
}
async function checkResponseTypeCount(req) {
await req
.post('/query')
.send({
"query": {},
'offsetsLimits': {
'songOffset': 0,
'songLimit': 10,
},
'ordering': {
'orderBy': {
'type': 'name',
},
'ascending': true
},
'responseType': 'count',
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
songs: 3,
artists: 0,
tags: 0,
albums: 0,
});
});
}
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200);
await helpers.createArtist(req, { name: "Artist2" }, 200);
await helpers.createSong(req, { title: "Song1", artistIds: [1], storeLinks: [ 'hello my', 'darling' ] }, 200);
await helpers.createSong(req, { title: "Song2", artistIds: [1] }, 200);
await helpers.createSong(req, { title: "Song3", artistIds: [2] }, 200);
await checkAllSongs(req);
await checkIdIn(req);
await checkIdNotIn(req);
await checkArtistIdIn(req);
await checkOrRelation(req);
await checkStoreLinksLike(req);
await checkResponseTypeCount(req);
await checkResponseTypeIds(req);
} finally {
req.close();
agent.close();
done();
}
});
});

@ -0,0 +1,203 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { expect } from 'chai';
import { SetupApp } from '../../../app';
import { ReferenceDatabase } from '../../reference_model/DBReferenceModel';
import { randomDBAction, RandomDBActionDistribution, DBActionType, applyReferenceDBAction, applyRealDBAction, DBAction } from '../../reference_model/randomGen';
import * as helpers from '../helpers';
import seedrandom from 'seedrandom';
import { AlbumWithRefsWithId, Artist, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs, TrackWithRefsWithId } from '../../../../client/src/api/api';
import sampleDB from '../sampleDB';
let stringify = require('json-stringify-deterministic');
let _ = require('lodash');
let tmp = require('tmp');
let fs = require('fs');
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
SetupApp(app, knex, '');
var agent = chai.request.agent(app);
return agent;
}
// Alters a response from a real or mock DB so that they can be deep-compared
// and only non-trivial differences trigger an error.
function normalizeResponse(response: any) {
let r: any = _.cloneDeep(response);
if (r && 'id' in r) {
r.id = '<redacted>';
}
return r;
}
// Alters a database export / reference database model so that it can be compared
// to another so that only non-trivial differences trigger an error.
function normalizeDB(oldDb: ReferenceDatabase) {
let db: ReferenceDatabase = _.cloneDeep(oldDb);
// Apply a deterministic sorting.
// TODO: sorting by name is not deterministic.
for (const userId in db) {
db[userId].tracks.sort((a: any, b: any) => a.name.localeCompare(b.name))
db[userId].albums.sort((a: any, b: any) => a.name.localeCompare(b.name))
db[userId].artists.sort((a: any, b: any) => a.name.localeCompare(b.name))
db[userId].tags.sort((a: any, b: any) => a.name.localeCompare(b.name))
}
// Re-map IDs.
interface IDMap {
map: Map<number, number>,
highestId: number,
};
let trackMap: IDMap = { map: new Map<number, number>(), highestId: 0 };
let albumMap: IDMap = { map: new Map<number, number>(), highestId: 0 };
let artistMap: IDMap = { map: new Map<number, number>(), highestId: 0 };
let tagMap: IDMap = { map: new Map<number, number>(), highestId: 0 };
let remapId = (id: number, map: IDMap) => {
if (map.map.has(id)) { return map.map.get(id) as number; }
let newId: number = map.highestId + 1;
map.map.set(id, newId);
map.highestId = newId;
return newId;
}
for (const userId in db) {
// First remap the IDs only, ignoring references
db[userId].tracks.forEach((x: TrackWithRefsWithId) => { console.log("X:", x); x.id = remapId(x.id, trackMap); });
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { x.id = remapId(x.id, albumMap); })
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { x.id = remapId(x.id, artistMap); })
db[userId].tags.forEach((x: TagWithRefsWithId) => { x.id = remapId(x.id, tagMap); })
}
for (const userId in db) {
// Now remap the references.
db[userId].tracks.forEach((x: TrackWithRefsWithId) => {
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap));
x.artistIds = x.artistIds.map((id: number) => remapId(id, artistMap));
x.albumId = x.albumId ? remapId(x.albumId, albumMap) : null;
});
db[userId].albums.forEach((x: AlbumWithRefsWithId) => {
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap));
x.artistIds = x.artistIds.map((id: number) => remapId(id, artistMap));
x.trackIds = x.trackIds.map((id: number) => remapId(id, trackMap));
});
db[userId].artists.forEach((x: ArtistWithRefsWithId) => {
x.tagIds = x.tagIds.map((id: number) => remapId(id, tagMap));
x.albumIds = x.albumIds.map((id: number) => remapId(id, albumMap));
x.trackIds = x.trackIds.map((id: number) => remapId(id, trackMap));
});
db[userId].tags.forEach((x: TagWithRefsWithId) => {
x.parentId = x.parentId ? remapId(x.parentId, tagMap) : null;
});
}
return db;
}
describe('Randomized model-based DB back-end tests', () => {
it('all succeed', async done => {
let req = await init();
let actionTrace: DBAction[] = [];
let seed: string = process.env.TEST_RANDOM_SEED || Math.random().toFixed(5).toString();
console.log(`Test random seed: '${seed}'`)
try {
// Create a reference DB
let refDB: ReferenceDatabase = _.cloneDeep(sampleDB);
// Prime the real DB
// First, create a user and log in.
await helpers.createUser(req, "someone@email.com", "password1A!", 200);
await helpers.login(req, "someone@email.com", "password1A!", 200);
// Import the starting DB.
await helpers.importDB(req, refDB[1]);
// Check that we are starting from an equal situation
let refState = normalizeDB(refDB);
let realState = normalizeDB({
[1]: (await helpers.getExport(req)).body,
});
expect(realState).to.deep.equal(refState);
// Start doing some random changes, checking the state after each step.
let rng = seedrandom(seed);
let dist: RandomDBActionDistribution = {
type: new Map([
[DBActionType.CreateTrack, 0.7],
[DBActionType.DeleteTrack, 0.3]
]),
userId: new Map([[1, 1.0]]),
createTrackParams: {
linkAlbum: new Map<boolean | 'nonexistent', number>([[false, 0.45], [true, 0.45], ['nonexistent', 0.1]]),
linkTags: {
numValid: new Map([[0, 1.0]]),
numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]),
},
linkArtists: {
numValid: new Map([[0, 1.0]]),
numInvalid: new Map([[0, 0.9], [1, 0.05], [2, 0.05]]),
},
},
deleteTrackParams: {
validTrack: new Map([[false, 0.2], [true, 0.8]])
}
}
for (let i = 0; i < 30; i++) {
let action = randomDBAction(
refDB,
rng,
dist
);
actionTrace.push(action);
console.log("Testing action: ", action);
let { response: refResponse, status: refStatus } = applyReferenceDBAction(action, refDB);
let { response: realResponse, status: realStatus } = await applyRealDBAction(action, req);
// Compare the response and status.
expect(normalizeResponse(realResponse)).to.deep.equal(normalizeResponse(refResponse));
expect(realStatus).to.equal(refStatus);
// Compare the database state after the action.
let refState = normalizeDB(refDB);
let realState = normalizeDB({
[1]: (await helpers.getExport(req)).body,
});
expect(realState).to.deep.equal(refState);
}
} catch (e) {
// When catching a comparison error, add and dump various states to files for debugging.
e.actionTrace = actionTrace;
e.startingDB = normalizeDB(sampleDB);
e.testSeed = seed;
if (e.actual && e.expected) {
e.actualDump = tmp.tmpNameSync();
e.expectedDump = tmp.tmpNameSync();
e.actionTraceDump = tmp.tmpNameSync();
e.startingDBDump = tmp.tmpNameSync();
fs.writeFileSync(e.actualDump, stringify(e.actual, { space: ' ' }));
fs.writeFileSync(e.expectedDump, stringify(e.expected, { space: ' ' }));
fs.writeFileSync(e.actionTraceDump, stringify(e.actionTrace, { space: ' ' }));
fs.writeFileSync(e.startingDBDump, stringify(e.startingDB, { space: ' ' }));
console.log(
"A comparison error occurred. Wrote compared values to temporary files for debugging:\n"
+ ` actual: ${e.actualDump}\n`
+ ` expected: ${e.expectedDump}\n`
+ ` DB action trace: ${e.actionTraceDump}\n`
+ ` Starting DB: ${e.startingDBDump}`
);
}
throw e;
} finally {
req.close();
done();
}
});
});

@ -1,131 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /song with no title', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with only a title', () => {
it('should return the first available id', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, { title: "MySong" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with a nonexistent artist Id', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with an existing artist Id', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 });
await helpers.createSong(req, { title: "MySong", artistIds: [1] }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with two existing artist Ids', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
await helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 })
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with an existent and a nonexistent artist Id', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
await helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400)
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /song with tags', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Root" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Leaf", parentId: 1 }, 200, { id: 2 })
await helpers.createSong(req, { title: "Song", tagIds: [1, 2] }, 200, { id: 1 })
await helpers.checkSong(req, 1, 200, { title: "Song", storeLinks: [], tagIds: [1, 2], albumIds: [], artistIds: [] })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -1,87 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
import { sha512 } from 'js-sha512';
async function init() {
chai.use(chaiHttp);
const app = express();
const knex = await helpers.initTestDB();
// Add test users.
await knex.insert({ email: "test1@test.com", passwordHash: sha512('pass1') }).into('users');
await knex.insert({ email: "test2@test.com", passwordHash: sha512('pass2') }).into('users');
SetupApp(app, knex, '');
// Login as a test user.
var agent = chai.request.agent(app);
await agent
.post('/login?username=' + encodeURIComponent("test1@test.com") + '&password=' + encodeURIComponent('pass1'))
.send({});
return agent;
}
describe('POST /tag with no name', () => {
it('should fail', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, {}, 400);
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /tag with a correct request', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "MyTag" }, 200, { id: 1 });
} finally {
req.close();
agent.close();
done();
}
});
});
describe('POST /tag with a parent', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 })
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});
describe('PUT /tag with a new parent', () => {
it('should succeed', async done => {
let agent = await init();
let req = agent.keepOpen();
try {
await helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
await helpers.createTag(req, { name: "Tag2" }, 200, { id: 2 })
await helpers.modifyTag(req, 2, { parentId: 1 }, 200)
await helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 })
} finally {
req.close();
agent.close();
done();
}
});
});

@ -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;
})
}

@ -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;

@ -1,7 +1,7 @@
{
"spec_dir": "test",
"spec_files": [
"**/*[fF]low.js"
"**/*[fF]low.[tj]s"
],
"helpers": [
"helpers/**/*.js"

@ -0,0 +1,116 @@
import { AlbumWithRefsWithId, ArtistWithRefsWithId, DBDataFormat, PostTrackRequest, TrackWithDetails, TrackWithRefsWithId } from "../../../client/src/api/api";
import { makeNotFoundError } from "../../db/common";
import filterInPlace from "../../lib/filterInPlace";
// The mock reference database is in the same format as
// the JSON import/export format, for multiple users.
export type ReferenceDatabase = Record<number, DBDataFormat>
type ObjectsType = "tracks" | "artists" | "tags" | "albums";
// Get a fresh ID for a new object.
function getNewId(db: ReferenceDatabase, objectsType: ObjectsType): number {
let highest: number = 1;
for (const data of Object.values(db)) {
data[objectsType].forEach((obj: any) => highest = Math.max(highest, obj.id));
}
return highest + 1;
}
// Check a (set of) IDs for presence in the objects array.
// All have to exist for it to return true.
function checkExists(objects: any[], ids: number[]) {
return ids.reduce((prev: boolean, id: number) => {
return prev && objects.find((object: any) => object.id === id);
}, true);
}
// If not in the array, put the number in the array.
function ensureInSet(n: number, s: number[]) {
if (!(n in s)) { s.push(n); }
}
// For a set of objects, ensure they point to another object.
function ensureLinked(fromObjects: number[], fromObjectsType: ObjectsType,
toId: number, toObjectsType: ObjectsType, data: DBDataFormat) {
if (toObjectsType === 'tracks') {
fromObjects.forEach((fromId: number) => ensureInSet(toId,
(data[fromObjectsType][fromId] as AlbumWithRefsWithId | ArtistWithRefsWithId).trackIds))
}
}
// Create a new object.
export interface LinkField { field: string, otherObjectType: ObjectsType };
export function createObject(
userId: number,
object: any,
objectType: ObjectsType,
singularLinkFields: LinkField[],
pluralLinkFields: LinkField[],
db: ReferenceDatabase
): { id: number } {
// Existence checks
if (!(userId in db)) { throw makeNotFoundError() }
singularLinkFields.forEach((f: LinkField) => {
if (!checkExists(db[userId][f.otherObjectType], object[f.field] ? [object[f.field]] : [])) {
throw makeNotFoundError();
}
});
pluralLinkFields.forEach((f: LinkField) => {
if (!checkExists(db[userId][f.otherObjectType], object[f.field] || [])) {
throw makeNotFoundError();
}
});
// Create an ID and the object
let id = getNewId(db, objectType);
db[userId][objectType].push({
...object,
id: id,
})
// reverse links
singularLinkFields.forEach((f: LinkField) => {
ensureLinked(object[f.field] ? [object[f.field]] : [], f.otherObjectType, id, objectType, db[userId]);
});
pluralLinkFields.forEach((f: LinkField) => {
ensureLinked(object[f.field] || [], f.otherObjectType, id, objectType, db[userId]);
});
return { id: id };
}
// Create a new track.
export function createTrack(userId: number, track: PostTrackRequest, db: ReferenceDatabase): { id: number } {
return createObject(
userId,
track,
'tracks',
[{ field: 'albumId', otherObjectType: 'albums' }],
[
{ field: 'artistIds', otherObjectType: 'artists' },
{ field: 'tagIds', otherObjectType: 'tags' },
],
db
);
}
// Delete a track.
export function deleteTrack(userId: number, id: number, db: ReferenceDatabase): void {
// Existence checks
if (!(userId in db)) { throw makeNotFoundError() }
// Find the object to delete.
let idx = db[userId].tracks.findIndex((track: TrackWithRefsWithId) => track.id === id);
if (idx < 0) {
// Not found
throw makeNotFoundError();
}
// Remove references
db[userId].albums.forEach((x: AlbumWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); })
db[userId].artists.forEach((x: ArtistWithRefsWithId) => { filterInPlace(x.trackIds, (tid: number) => tid !== id); })
// Delete the object
db[userId].tracks.splice(idx, 1);
}

@ -0,0 +1,238 @@
import { AlbumWithRefsWithId, ArtistWithRefsWithId, TagWithRefsWithId, TrackWithRefs } from "../../../client/src/api/api";
import { userEndpoints } from "../../endpoints/User";
import { createTrack, deleteTrack, ReferenceDatabase } from "./DBReferenceModel";
import * as helpers from '../integration/helpers';
import { DBErrorKind, isDBError } from "../../endpoints/types";
export enum DBActionType {
CreateTrack = 0,
DeleteTrack,
}
export interface DBAction {
type: DBActionType,
userId: number,
payload: any,
}
export type Distribution<T> = Map<T, number>;
export interface RandomDBActionDistribution {
type: Distribution<DBActionType>,
userId: Distribution<number>,
createTrackParams: RandomCreateTrackDistribution,
deleteTrackParams: RandomDeleteTrackDistribution,
}
export interface RandomCreateTrackDistribution {
linkArtists: {
numValid: Distribution<number>,
numInvalid: Distribution<number>,
}
linkTags: {
numValid: Distribution<number>,
numInvalid: Distribution<number>,
}
linkAlbum: Distribution<boolean | 'nonexistent'>,
}
export interface RandomDeleteTrackDistribution {
validTrack: Distribution<boolean>,
}
export function applyDistribution<T>(
dist: Map<T, number>,
randomNumGen: any,
): T {
let n = randomNumGen();
let r: T | undefined = undefined;
dist.forEach((value: number, key: T) => {
if (r) { return; }
if (n <= value) { r = key; }
else { n -= value; }
})
if (r === undefined) {
throw new Error(`Invalid distribution: n=${n}, dist ${JSON.stringify(dist.entries())}`);
}
return r;
}
export function randomString(randomNumGen: any, length: number) {
let chars = 'abcdefghijklmnopqrstuvwxyz';
let retval = '';
for (let i = 0; i < length; i++) {
retval += chars[Math.floor(randomNumGen() * 26)];
}
return retval;
}
export function pickNFromArray<T>(
array: T[],
randomNumGen: any,
N: number)
: T[] {
let r: T[] = [];
for (let i = 0; i < N; i++) {
let idx = Math.floor(randomNumGen() * array.length);
r.push(array[idx]);
array.splice(idx);
}
return r;
}
export function applyReferenceDBAction(
action: DBAction,
db: ReferenceDatabase
): {
response: any,
status: number,
} {
let response: any = undefined;
let status: number = 0;
try {
switch (action.type) {
case DBActionType.CreateTrack: {
response = createTrack(action.userId, action.payload, db);
status = 200;
break;
}
case DBActionType.DeleteTrack: {
deleteTrack(action.userId, action.payload, db);
response = {};
status = 200;
break;
}
}
} catch(e) {
if(isDBError(e)) {
if(e.kind === DBErrorKind.ResourceNotFound) {
status = 404;
response = {};
}
}
}
return { response: response, status: status };
}
export async function applyRealDBAction(
action: DBAction,
req: any,
): Promise<{
response: any,
status: number,
}> {
let response: any = undefined;
let status: number = 0;
switch (action.type) {
case DBActionType.CreateTrack: {
let res = await helpers.createTrack(req, action.payload);
status = res.status;
response = res.body;
break;
}
case DBActionType.DeleteTrack: {
let res = await helpers.deleteTrack(req, action.payload);
status = res.status;
response = res.body;
break;
}
}
return { response: response, status: status };
}
export function randomDBAction(
db: ReferenceDatabase,
randomNumGen: any,
distribution: RandomDBActionDistribution,
): DBAction {
let type = applyDistribution(
distribution.type,
randomNumGen
);
let userId = applyDistribution(
distribution.userId,
randomNumGen
);
switch (type) {
case DBActionType.CreateTrack: {
return {
type: type,
payload: createRandomTrack(
db,
userId,
distribution.createTrackParams,
randomNumGen
),
userId: userId,
};
}
case DBActionType.DeleteTrack: {
return {
type: type,
payload: applyDistribution(distribution.deleteTrackParams.validTrack, randomNumGen) ?
Math.floor(Math.random() * db[userId].tracks.length) + 1 :
Math.floor(Math.random() * db[userId].tracks.length) + 1 + db[userId].tracks.length,
userId: userId,
}
}
}
}
export function createRandomTrack(
db: ReferenceDatabase,
userId: number,
trackDist: RandomCreateTrackDistribution,
randomNumGen: any,
): TrackWithRefs {
let allValidArtistIds: number[] = db[userId] && db[userId].artists ?
db[userId].artists.map((a: ArtistWithRefsWithId) => a.id) : [];
let allValidTagIds: number[] = db[userId] && db[userId].tags ?
db[userId].tags.map((a: TagWithRefsWithId) => a.id) : [];
let allValidAlbumIds: number[] = db[userId] && db[userId].albums ?
db[userId].albums.map((a: AlbumWithRefsWithId) => a.id) : [];
let artists: number[] = (() => {
let validArtists: number[] = pickNFromArray(allValidArtistIds, randomNumGen, applyDistribution(trackDist.linkArtists.numValid, randomNumGen));
let invalidArtists: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkArtists.numInvalid, randomNumGen); i++) {
invalidArtists.push(Math.round(Math.random() * 100) + allValidArtistIds.length);
}
return [...validArtists, ...invalidArtists];
})();
let tags: number[] = (() => {
let validTags: number[] = pickNFromArray(allValidTagIds, randomNumGen, applyDistribution(trackDist.linkTags.numValid, randomNumGen));
let invalidTags: number[] = [];
for (let i = 0; i < applyDistribution(trackDist.linkTags.numInvalid, randomNumGen); i++) {
invalidTags.push(Math.round(Math.random() * 100) + allValidTagIds.length);
}
return [...validTags, ...invalidTags];
})();
let maybeAlbum: number | null = (() => {
let r: boolean | null | 'nonexistent' = applyDistribution(trackDist.linkAlbum, randomNumGen);
let maybeValidAlbum: number | null =
r === true &&
allValidAlbumIds.length ?
pickNFromArray(allValidAlbumIds, randomNumGen, 1)[0] :
null;
let maybeInvalidAlbum: number | null =
r === 'nonexistent' ?
allValidAlbumIds.length + 1 : null;
return maybeValidAlbum || maybeInvalidAlbum;
})();
return {
mbApi_typename: 'track',
albumId: maybeAlbum,
artistIds: artists,
tagIds: tags,
name: randomString(randomNumGen, 20),
storeLinks: [], // TODO
}
}
Loading…
Cancel
Save