Add tags and basic test for it.

pull/7/head
Sander Vocke 5 years ago
parent 535ff867a9
commit 77a116938c
  1. 46
      client/src/api.ts
  2. 6
      server/app.ts
  3. 48
      server/endpoints/CreateTagEndpointHandler.ts
  4. 51
      server/endpoints/ModifyTagEndpointHandler.ts
  5. 39
      server/endpoints/TagDetailsEndpointHandler.ts
  6. 5
      server/models/album.js
  7. 3
      server/models/artist.js
  8. 1
      server/models/song.js
  9. 14
      server/models/tag.js
  10. 39
      server/test/integration/flows/ArtistFlow.js
  11. 107
      server/test/integration/flows/CreateSongFlow.js
  12. 91
      server/test/integration/flows/SongFlow.js
  13. 23
      server/test/integration/flows/TagFlow.js
  14. 43
      server/test/integration/flows/helpers.js

@ -143,3 +143,49 @@ export interface ModifyArtistResponse {}
export function checkModifyArtistRequest(req:any): boolean { export function checkModifyArtistRequest(req:any): boolean {
return true; return true;
} }
// Create a new tag (POST).
export const CreateTagEndpoint = '/tag';
export interface CreateTagRequest {
name: String;
parentId?: Number;
}
export interface CreateTagResponse {
id: Number;
}
export function checkCreateTagRequest(req:any): boolean {
return "body" in req &&
"name" in req.body;
}
// Modify an existing tag (PUT).
export const ModifyTagEndpoint = '/tag/:id';
export interface ModifyTagRequest {
name?: String,
parentId?: Number;
}
export interface ModifyTagResponse {}
export function checkModifyTagRequest(req:any): boolean {
return true;
}
// Query for tags.
export const QueryTagEndpoint = '/tag/query';
export interface QueryTagsRequest {}
export interface QueryTagsResponse {
ids: Number[]
}
export function checkQueryTagsRequest(req:any): boolean {
return true;
}
// Get tag details (GET).
export const TagDetailsEndpoint = '/tag/:id';
export interface TagDetailsRequest {}
export interface TagDetailsResponse {
name: String,
parentId?: Number,
}
export function checkTagDetailsRequest(req:any): boolean {
return true;
}

@ -9,6 +9,9 @@ import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetailsEndpointH
import { SongDetailsEndpointHandler } from './endpoints/SongDetailsEndpointHandler'; import { SongDetailsEndpointHandler } from './endpoints/SongDetailsEndpointHandler';
import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler'; import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler';
import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler'; import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler';
import { CreateTagEndpointHandler } from './endpoints/CreateTagEndpointHandler';
import { ModifyTagEndpointHandler } from './endpoints/ModifyTagEndpointHandler';
import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler';
import * as endpointTypes from './endpoints/types'; import * as endpointTypes from './endpoints/types';
const invokeHandler = (handler:endpointTypes.EndpointHandler) => { const invokeHandler = (handler:endpointTypes.EndpointHandler) => {
@ -38,6 +41,9 @@ const SetupApp = (app: any) => {
app.put(api.ModifySongEndpoint, invokeHandler(ModifySongEndpointHandler)); app.put(api.ModifySongEndpoint, invokeHandler(ModifySongEndpointHandler));
app.get(api.SongDetailsEndpoint, invokeHandler(SongDetailsEndpointHandler)); app.get(api.SongDetailsEndpoint, invokeHandler(SongDetailsEndpointHandler));
app.get(api.ArtistDetailsEndpoint, invokeHandler(ArtistDetailsEndpointHandler)); app.get(api.ArtistDetailsEndpoint, invokeHandler(ArtistDetailsEndpointHandler));
app.post(api.CreateTagEndpoint, invokeHandler(CreateTagEndpointHandler));
app.put(api.ModifyTagEndpoint, invokeHandler(ModifyTagEndpointHandler));
app.get(api.TagDetailsEndpoint, invokeHandler(TagDetailsEndpointHandler));
} }
export { SetupApp } export { SetupApp }

@ -0,0 +1,48 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
export const CreateTagEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkCreateTagRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid CreateTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.CreateTagRequest = req.body;
const getTag = async (id: Number) => {
const tag = await models.Tag.findAll({
where: {
id: id
}
});
if (tag.length != 1) {
const e: EndpointError = {
internalMessage: 'There is no tag with id ' + id + '.',
httpStatus: 400
};
throw e;
}
return tag[0];
}
// If applicable, start retrieving the new parent tag.
const maybeNewParentPromise: Promise<any> | Promise<undefined> =
(reqObject.parentId) ? getTag(reqObject.parentId) : (async () => { return undefined })();
(async () => {
const maybeParent = await maybeNewParentPromise;
const tag = await models.Tag.create({
name: reqObject.name
});
reqObject.parentId && await tag.setParent(maybeParent);
await tag.save();
const responseObject: api.CreateTagResponse = {
id: tag.id
};
res.status(200).send(responseObject);
})()
.catch(catchUnhandledErrors);
}

@ -0,0 +1,51 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
import tag from '../models/tag';
export const ModifyTagEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkModifySongRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid ModifyTag request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
const reqObject: api.ModifyTagRequest = req.body;
const getTag = async (id:Number) => {
const tag = await models.Tag.findAll({
where: {
id: id
}
});
if(tag.length != 1) {
const e: EndpointError = {
internalMessage: 'There is no tag with id ' + id + '.',
httpStatus: 400
};
throw e;
}
return tag[0];
}
// If applicable, start retrieving the new parent tag.
const maybeNewParentPromise:Promise<any>|Promise<undefined> =
(reqObject.parentId) ? getTag(reqObject.parentId) : (async () => { return undefined })();
// Start retrieving the tag to modify.
var tagInstancePromise: Promise<any> = getTag(req.params.id);
// Upon finish retrieving artists and albums, modify the song.
await Promise.all([maybeNewParentPromise, tagInstancePromise])
.then(async (values: any) => {
var [maybeParent, tag] = values;
if(reqObject.name) { tag.setName(reqObject.name) };
if(reqObject.parentId) { tag.setParent(maybeParent) };
await tag.save();
})
.then(() => {
res.status(200).send({});
})
.catch(catchUnhandledErrors);
}

@ -0,0 +1,39 @@
const models = require('../models');
import * as api from '../../client/src/api';
import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types';
export const TagDetailsEndpointHandler: EndpointHandler = async (req: any, res: any) => {
if (!api.checkTagDetailsRequest(req)) {
const e: EndpointError = {
internalMessage: 'Invalid TagDetails request: ' + JSON.stringify(req.body),
httpStatus: 400
};
throw e;
}
await models.Tag.findAll({
where: {
id: req.params.id
},
include: [{
model: models.Tag,
as: 'parent'
}]
})
.then((tags: any[]) => {
if (tags.length != 1) {
const e: EndpointError = {
internalMessage: 'There is no tag with id ' + req.params.id + '.',
httpStatus: 400
};
throw e;
}
let tag = tags[0];
var response: api.TagDetailsResponse = {
name: tag.name,
};
if(tag.parent) { response['parentId'] = tag.parent.id; }
res.send(response);
})
.catch(catchUnhandledErrors);
}

@ -5,8 +5,9 @@ module.exports = (sequelize, DataTypes) => {
}); });
Album.associate = function (models) { Album.associate = function (models) {
models.Album.belongsToMany(models.Song, { through: "SongAlbums" }) models.Album.belongsToMany(models.Song, { through: "SongAlbums" });
models.Album.belongsToMany(models.Artist, { through: "AlbumArtists" }) models.Album.belongsToMany(models.Artist, { through: "AlbumArtists" });
models.Album.belongsToMany(models.Tag, { through: 'AlbumTags' });
}; };
return Album; return Album;

@ -6,7 +6,8 @@ module.exports = (sequelize, DataTypes) => {
Artist.associate = function (models) { Artist.associate = function (models) {
models.Artist.belongsToMany(models.Song, { through: "SongArtists" }); models.Artist.belongsToMany(models.Song, { through: "SongArtists" });
models.Artist.belongsToMany(models.Album, { through: "AlbumArtists" }) models.Artist.belongsToMany(models.Album, { through: "AlbumArtists" });
models.Artist.belongsToMany(models.Tag, { through: 'ArtistTags' });
}; };
return Artist; return Artist;

@ -7,6 +7,7 @@ module.exports = (sequelize, DataTypes) => {
Song.associate = function (models) { Song.associate = function (models) {
models.Song.belongsToMany(models.Artist, { through: "SongArtists" }); models.Song.belongsToMany(models.Artist, { through: "SongArtists" });
models.Song.belongsToMany(models.Album, { through: "SongAlbums" }); models.Song.belongsToMany(models.Album, { through: "SongAlbums" });
models.Song.belongsToMany(models.Tag, { through: 'SongTags' });
}; };
return Song; return Song;

@ -0,0 +1,14 @@
module.exports = (sequelize, DataTypes) => {
var Tag = sequelize.define('Tag', {
name: DataTypes.STRING,
});
Tag.associate = function (models) {
models.Tag.hasOne(models.Tag, { as: 'parent' });
models.Tag.belongsToMany(models.Artist, { through: 'ArtistTags' });
models.Tag.belongsToMany(models.Album, { through: 'AlbumTags' });
models.Tag.belongsToMany(models.Song, { through: 'SongTags' });
};
return Tag;
};

@ -14,6 +14,42 @@ async function init() {
return app; return app;
} }
describe('POST /artist with no name', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/artist')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
});
});
});
describe('POST /artist with a correct request', () => {
it('should succeed', done => {
init().then((app) => {
chai
.request(app)
.post('/artist')
.send({
name: "MyArtist"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
});
});
});
describe('PUT /artist on nonexistent artist', () => { describe('PUT /artist on nonexistent artist', () => {
it('should fail', done => { it('should fail', done => {
init().then((app) => { init().then((app) => {
@ -38,9 +74,10 @@ describe('PUT /artist with an existing artist', () => {
var req = chai.request(app).keepOpen(); var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 }) helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200) ) .then(() => helpers.modifyArtist(req, 1, { name: "MyNewArtist" }, 200) )
.then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist" } ) ) .then(() => helpers.checkArtist(req, 1, 200, { name: "MyNewArtist", storeLinks: [] } ) )
.then(req.close) .then(req.close)
.then(done); .then(done);
}); });
}); });
}); });

@ -1,107 +0,0 @@
const chai = require('chai');
const chaiHttp = require('chai-http');
const express = require('express');
const models = require('../../../models');
import { SetupApp } from '../../../app';
import { expect } from 'chai';
import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app);
await models.sequelize.sync({ force: true });
return app;
}
describe('POST /song with no title', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/song')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
});
});
describe('POST /song with only a title', () => {
it('should return the first available id', done => {
init().then(async(app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
})
});
});
describe('POST /song with a nonexistent artist Id', () => {
it('should fail', done => {
init().then(async (app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong",
artistIds: [1]
})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
});
});
describe('POST /song with an existing artist Id', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [ 1 ] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
});
});
describe('POST /song with two existing artist Ids', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) )
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
});
});
describe('POST /song with an existent and a nonexistent artist Id', () => {
it('should fail', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) )
.then(req.close)
.then(done);
});
});
});
export { };

@ -14,6 +14,97 @@ async function init() {
return app; return app;
} }
describe('POST /song with no title', () => {
it('should fail', done => {
init().then((app) => {
chai
.request(app)
.post('/song')
.send({})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
});
});
describe('POST /song with only a title', () => {
it('should return the first available id', done => {
init().then(async(app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong"
})
.then((res) => {
expect(res).to.have.status(200);
expect(res.body).to.deep.equal({
id: 1
});
done();
});
})
});
});
describe('POST /song with a nonexistent artist Id', () => {
it('should fail', done => {
init().then(async (app) => {
chai
.request(app)
.post('/song')
.send({
title: "MySong",
artistIds: [1]
})
.then((res) => {
expect(res).to.have.status(400);
done();
});
})
});
});
describe('POST /song with an existing artist Id', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "MyArtist" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [ 1 ] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
});
});
describe('POST /song with two existing artist Ids', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createArtist(req, { name: "Artist2" }, 200, { id: 2 }) )
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 200, { id: 1 }) )
.then(req.close)
.then(done);
});
});
});
describe('POST /song with an existent and a nonexistent artist Id', () => {
it('should fail', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createArtist(req, { name: "Artist1" }, 200, { id: 1 })
.then(() => helpers.createSong(req, { title: "MySong", artistIds: [1, 2] }, 400) )
.then(req.close)
.then(done);
});
});
});
describe('POST /song/query with no songs', () => { describe('POST /song/query with no songs', () => {
it('should give empty list', done => { it('should give empty list', done => {
init().then((app) => { init().then((app) => {

@ -14,12 +14,12 @@ async function init() {
return app; return app;
} }
describe('POST /artist with no name', () => { describe('POST /tag with no name', () => {
it('should fail', done => { it('should fail', done => {
init().then((app) => { init().then((app) => {
chai chai
.request(app) .request(app)
.post('/artist') .post('/tag')
.send({}) .send({})
.then((res) => { .then((res) => {
expect(res).to.have.status(400); expect(res).to.have.status(400);
@ -29,14 +29,14 @@ describe('POST /artist with no name', () => {
}); });
}); });
describe('POST /artist with a correct request', () => { describe('POST /tag with a correct request', () => {
it('should succeed', done => { it('should succeed', done => {
init().then((app) => { init().then((app) => {
chai chai
.request(app) .request(app)
.post('/artist') .post('/tag')
.send({ .send({
name: "MyArtist" name: "MyTag"
}) })
.then((res) => { .then((res) => {
expect(res).to.have.status(200); expect(res).to.have.status(200);
@ -48,3 +48,16 @@ describe('POST /artist with a correct request', () => {
}); });
}); });
}); });
describe('POST /tag with a parent', () => {
it('should succeed', done => {
init().then((app) => {
var req = chai.request(app).keepOpen();
helpers.createTag(req, { name: "Tag1" }, 200, { id: 1 })
.then(() => helpers.createTag(req, { name: "Tag2", parentId: 1 }, 200, { id: 2 }) )
.then(() => helpers.checkTag(req, 2, 200, { name: "Tag2", parentId: 1 }))
.then(req.close)
.then(done);
});
});
});

@ -57,3 +57,46 @@ export async function checkArtist(
expectResponse && expect(res.body).to.deep.equal(expectResponse); expectResponse && expect(res.body).to.deep.equal(expectResponse);
}) })
} }
export async function createTag(
req,
props = { name: "Tag" },
expectStatus = undefined,
expectResponse = undefined
) {
await req
.post('/tag')
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
});
}
export async function modifyTag(
req,
id = 1,
props = { name: "NewTag" },
expectStatus = undefined,
) {
await req
.put('/tag/' + id)
.send(props)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
});
}
export async function checkTag(
req,
id,
expectStatus = undefined,
expectResponse = undefined,
) {
await req
.get('/tag/' + id)
.then((res) => {
expectStatus && expect(res).to.have.status(expectStatus);
expectResponse && expect(res.body).to.deep.equal(expectResponse);
})
}
Loading…
Cancel
Save