Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] challenges and dits #66

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
create tests for dit-tags and make them pass
agatatalita committed Apr 17, 2018
commit c8b2146b7ebe2afb2e73c54366d625606c9e2bfb
1 change: 1 addition & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -75,6 +75,7 @@ app.use('/challenges', require('./routes/challenges'));
// vote for ideas, ...
app.use('/ideas', require('./routes/votes'));
app.use('/comments', require('./routes/votes'));
app.use('/challenges', require('./routes/votes'));

// following are route factories
// they need to know what is the primary object (i.e. idea, comment, etc.)
34 changes: 22 additions & 12 deletions controllers/dit-tags.js
Original file line number Diff line number Diff line change
@@ -10,16 +10,16 @@ const models = require(path.resolve('./models')),
* Adds a tag to a dit
*/
async function post(req, res, next) {
let ditType;
try {
// gather data from request
const { ditType } = req.body;
const { tagname } = req.body.tag;
const ditId = req.params.id;
const username = req.auth.username;

ditType = req.baseUrl.slice(1,-1);
// save new dit-tag to database
const newDitTag = await models.ditTag.create(ditType, ditId, tagname, { }, username);

// serialize response body
let responseBody;
switch(ditType){
@@ -33,13 +33,9 @@ async function post(req, res, next) {
}
}

// serialize response body
// const responseBody = serialize.ditTag(newDitTag);

// respond
return res.status(201).json(responseBody);
} catch (e) {

// handle errors
switch (e.code) {
// duplicate dit-tag
@@ -54,7 +50,7 @@ async function post(req, res, next) {
// dit creator is not me
case 403: {
return res.status(403).json({ errors: [
{ status: 403, detail: 'not logged in as dit creator' }
{ status: 403, detail: `not logged in as ${ditType} creator` }
]});
}
// unexpected error
@@ -71,15 +67,27 @@ async function post(req, res, next) {
* GET /dits/:id/tags
*/
async function get(req, res, next) {
let ditType;
try {
// read dit id
const { id } = req.params;
ditType = req.baseUrl.slice(1, -1);

// read ditTags from database
const ditTags = await models.ditTag.readTagsOfDit(id);
const newDitTags = await models.ditTag.readTagsOfDit(ditType, id);

// serialize response body
const responseBody = serialize.ditTag(ditTags);
let responseBody;
switch(ditType){
case 'idea': {
responseBody = serialize.ideaTag(newDitTags);
break;
}
case 'challenge': {
responseBody = serialize.challengeTag(newDitTags);
break;
}
}

// respond
return res.status(200).json(responseBody);
@@ -88,7 +96,7 @@ async function get(req, res, next) {
if (e.code === 404) {
return res.status(404).json({ errors: [{
status: 404,
detail: '${ditType} not found'
detail: `${ditType} not found`
}] });
}

@@ -102,11 +110,13 @@ async function get(req, res, next) {
* DELETE /dits/:id/tags/:tagname
*/
async function del(req, res, next) {
let ditType;
try {
const { id, tagname } = req.params;
const { username } = req.auth;
ditType = req.baseUrl.slice(1, -1);

await models.ditTag.remove(id, tagname, username);
await models.ditTag.remove(ditType, id, tagname, username);

return res.status(204).end();
} catch (e) {
@@ -116,7 +126,7 @@ async function del(req, res, next) {
}
case 403: {
return res.status(403).json({ errors: [
{ status: 403, detail: 'not logged in as ${ditType} creator' }
{ status: 403, detail: `not logged in as ${ditType} creator` }
] });
}
default: {
4 changes: 3 additions & 1 deletion controllers/dits.js
Original file line number Diff line number Diff line change
@@ -10,8 +10,10 @@ const path = require('path'),
async function post(req, res, next) {
try {
// gather data
const { title, detail, ditType } = req.body;
const { title, detail } = req.body;
const creator = req.auth.username;
const ditType = req.baseUrl.slice(1,-1);

// save the dit to database
const newDit = await models.dit.create(ditType, { title, detail, creator });

3 changes: 2 additions & 1 deletion controllers/validators/schema/index.js
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ const account = require('./account'),
authenticate = require('./authenticate'),
avatar = require('./avatar'),
challenges = require('./challenges'),
challengeTags = require('./challenge-tags'),
comments = require('./comments'),
contacts = require('./contacts'),
definitions = require('./definitions'),
@@ -17,5 +18,5 @@ const account = require('./account'),
votes = require('./votes');


module.exports = Object.assign({ definitions }, account, authenticate, avatar, challenges,
module.exports = Object.assign({ definitions }, account, authenticate, avatar, challenges, challengeTags,
comments, contacts, ideas, ideaTags, messages, params, tags, users, userTags, votes);
150 changes: 83 additions & 67 deletions models/dit-tag/index.js
Original file line number Diff line number Diff line change
@@ -4,14 +4,18 @@ const path = require('path');

const Model = require(path.resolve('./models/model')),
schema = require('./schema');
const ditsDictionary = { challenge: 'challenge', idea: 'idea' };

class DitTag extends Model {

class DitTag extends Model {
/**
* Create ditTag in database
*/
static async create(ditType, ditId, tagname, ditTagInput, creatorUsername) {
// generate standard ideaTag
// allow just particular strings for a ditType
ditType = ditsDictionary[ditType];

// generate standard ditTag
const ditTag = await schema(ditTagInput);
// / STOPPED
const query = `
@@ -22,15 +26,15 @@ class DitTag extends Model {
// array of users (1 or 0)
LET us = (FOR u IN users FILTER u.username == @creatorUsername RETURN u)
// create the ditTag (if dit, tag and creator exist)
LET ditTag = (FOR d IN ds FOR t IN ts FOR u IN us FILTER u._id == d.creator
LET ${ditType}Tag = (FOR d IN ds FOR t IN ts FOR u IN us FILTER u._id == d.creator
INSERT MERGE({ _from: d._id, _to: t._id, creator: u._id }, @ditTag) IN ${ditType}Tags RETURN KEEP(NEW, 'created'))[0] || { }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name of the collection can (and should) be provided with placeholder @@ditTypeTags (two @ refer to collection name)

and then const params = { '@ditTypeTags': ditType + 'Tags' }

also above for ${ditType}.
This won't work for variable names, though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote in the comment that it is not done yet.

// if ditTag was not created, default to empty object (to be able to merge later)
// gather needed data
LET creator = MERGE(KEEP(us[0], 'username'), us[0].profile)
LET tag = KEEP(ts[0], 'tagname')
LET dit = MERGE(KEEP(us[0], 'title', 'detail'), { id: us[0]._key })
LET ${ditType} = MERGE(KEEP(ds[0], 'title', 'detail'), { id: ds[0]._key })
// return data
RETURN MERGE(ditTag, { creator, tag, dit })`;
RETURN MERGE(${ditType}Tag, { creator, tag, ${ditType} })`;

const params = { ditId, tagname, ditTag, creatorUsername };

@@ -51,17 +55,17 @@ class DitTag extends Model {

function generateError(response) {
let e;
// check that idea, tag and creator exist
const { dit, tag, creator } = response;

// check that dit, tag and creator exist
// some of them don't exist, then ditTag was not created
if (!(dit && tag && creator)) {
if (!(response[`${ditType}`] && response['tag'] && response['creator'])) {
e = new Error('Not Found');
e.code = 404;
e.missing = [];

['dit', 'tag', 'creator'].forEach((potentialMissing) => {
if (!response[potentialMissing]) e.missing.push(potentialMissing);
[`${ditType}`, 'tag', 'creator'].forEach((potentialMissing) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`${}` is not necessary here. (also on line 60)

if (!response[potentialMissing]){
e.missing.push(potentialMissing);
}
});
} else {
// if all exist, then dit creator !== ditTag creator, not authorized
@@ -75,100 +79,112 @@ class DitTag extends Model {
}

/**
* Read ideaTag from database
* Read ditTag from database
*/
static async read(ideaId, tagname) {
static async read(ditType, ditId, tagname) {
const ditCollection = ditType + 's';
const ditTags = ditType + 'Tags';
// allow just particular strings for a ditType
ditType = ditsDictionary[ditType];

const query = `
FOR t IN tags FILTER t.tagname == @tagname
FOR i IN ideas FILTER i._key == @ideaId
FOR it IN ideaTags FILTER it._from == i._id AND it._to == t._id
LET creator = (FOR u IN users FILTER u._id == it.creator
FOR d IN @@ditCollection FILTER d._key == @ditId
FOR dt IN @@ditTags FILTER dt._from == d._id AND dt._to == t._id
LET creator = (FOR u IN users FILTER u._id == dt.creator
RETURN MERGE(KEEP(u, 'username'), u.profile))[0]
LET ideaTag = KEEP(it, 'created')
LET ${ditType}Tag = KEEP(dt, 'created')
LET tag = KEEP(t, 'tagname')
LET idea = MERGE(KEEP(i, 'title', 'detail'), { id: i._key })
RETURN MERGE(ideaTag, { creator, tag, idea })`;
const params = { ideaId, tagname };

LET ${ditType} = MERGE(KEEP(d, 'title', 'detail'), { id: d._key })
RETURN MERGE(${ditType}Tag, { creator, tag, ${ditType} })`;
const params = { ditId, tagname, '@ditCollection': ditCollection, '@ditTags': ditTags };
const cursor = await this.db.query(query, params);

return (await cursor.all())[0];
}

/**
* Read tags of idea
* Read tags of dit
*/
static async readTagsOfIdea(ideaId) {
static async readTagsOfDit(ditType, ditId) {
const ditCollection = ditType + 's';
const ditTag = ditType + 'Tags';
// allow just particular strings for a ditType
ditType = ditsDictionary[ditType];

const query = `
// read idea into array (length 1 or 0)
LET is = (FOR i IN ideas FILTER i._key == @ideaId RETURN i)
// read ideaTags
LET its = (FOR i IN is
FOR it IN ideaTags FILTER it._from == i._id
FOR t IN tags FILTER it._to == t._id
// read dit into array (length 1 or 0)
LET ds = (FOR d IN @@ditCollection FILTER d._key == @ditId RETURN d)
// read ditTags
LET dts = (FOR d IN ds
FOR dt IN @@ditTag FILTER dt._from == d._id
FOR t IN tags FILTER dt._to == t._id
SORT t.tagname
LET ideaTag = KEEP(it, 'created')
LET ${ditType}Tag = KEEP(dt, 'created')
LET tag = KEEP(t, 'tagname')
LET idea = MERGE(KEEP(i, 'title', 'detail'), { id: i._key })
RETURN MERGE(ideaTag, { tag, idea })
LET ${ditType} = MERGE(KEEP(d, 'title', 'detail'), { id: d._key })
RETURN MERGE(${ditType}Tag, { tag, ${ditType} })
)
RETURN { ideaTags: its, idea: is[0] }`;
const params = { ideaId };
RETURN { ${ditType}Tags: dts, ${ditType}: ds[0] }`;
const params = { ditId, '@ditCollection': ditCollection, '@ditTag':ditTag };

const cursor = await this.db.query(query, params);

const [{ idea, ideaTags }] = await cursor.all();

// when idea not found, error
if (!idea) {
const e = new Error('idea not found');
// const [{ dit, ditTags }] = await cursor.all();
const ditTagsData = await cursor.all();
// when dit not found, error
if (!ditTagsData[0][`${ditType}`]) {
const e = new Error(`${ditType} not found`);
e.code = 404;
throw e;
}

return ideaTags;
return ditTagsData[0][`${ditType}Tags`];
}

/**
* Remove ideaTag from database
* Remove ditTag from database
*/
static async remove(ideaId, tagname, username) {
static async remove(ditType, ditId, tagname, username) {
const ditCollection = ditType + 's';
const ditTags = ditType + 'Tags';
// allow just particular strings for a ditType
ditType = ditsDictionary[ditType];

const query = `
// find users (1 or 0)
LET us = (FOR u IN users FILTER u.username == @username RETURN u)
// find ideas (1 or 0)
LET is = (FOR i IN ideas FILTER i._key == @ideaId RETURN i)
// find [ideaTag] between idea and tag specified (1 or 0)
LET its = (FOR i IN is
// find dits (1 or 0)
LET ds = (FOR i IN @@ditCollection FILTER i._key == @ditId RETURN i)
// find [ditTag] between dit and tag specified (1 or 0)
LET dts = (FOR i IN ds
FOR t IN tags FILTER t.tagname == @tagname
FOR it IN ideaTags FILTER it._from == i._id AND it._to == t._id
RETURN it)
// find and remove [ideaTag] if and only if user is creator of idea
// is user authorized to remove the ideaTag in question?
LET itsdel = (FOR u IN us FOR i IN is FILTER u._id == i.creator
FOR it IN its
REMOVE it IN ideaTags
RETURN it)
// return [ideaTag] between idea and tag
RETURN its`;

const params = { ideaId, tagname, username };
FOR dt IN @@ditTags FILTER dt._from == i._id AND dt._to == t._id
RETURN dt)
// find and remove [ditTag] if and only if user is creator of dit
// is user authorized to remove the ditTag in question?
LET dtsdel = (FOR u IN us FOR d IN ds FILTER u._id == d.creator
FOR dt IN dts
REMOVE dt IN @@ditTags
RETURN dt)
// return [ditTag] between dit and tag
RETURN dts`;

const params = { ditId, tagname, username, '@ditTags': ditTags, '@ditCollection': ditCollection};

// execute query and gather database response
const cursor = await this.db.query(query, params);
const [matchedIdeaTags] = await cursor.all();
const [matchedDitTags] = await cursor.all();

// return or error
switch (cursor.extra.stats.writesExecuted) {
// ideaTag was removed: ok
// ditTag was removed: ok
case 1: {
return;
}
// ideaTag was not removed: error
// ditTag was not removed: error
case 0: {
throw generateError(matchedIdeaTags);
throw generateError(matchedDitTags);
}
// unexpected error
default: {
@@ -177,19 +193,19 @@ class DitTag extends Model {
}

/**
* When no ideaTag was removed, it can have 2 reasons:
* 1. ideaTag was not found
* 2. ideaTag was found, but the user is not creator of the idea
* When no ditTag was removed, it can have 2 reasons:
* 1. ditTag was not found
* 2. ditTag was found, but the user is not creator of the dit
* therefore is not authorized to do so
*/
function generateError(response) {
let e;
if (response.length === 0) {
// ideaTag was not found
// ditTag was not found
e = new Error('not found');
e.code = 404;
} else {
// ideaTag was found, but user is not idea's creator
// ditTag was found, but user is not dit's creator
e = new Error('not authorized');
e.code = 403;
}
3 changes: 2 additions & 1 deletion serializers/index.js
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
const Deserializer = require('jsonapi-serializer').Deserializer;

const challenges = require('./challenges'),
challengeTags = require('./challenge-tags'),
comments = require('./comments'),
contacts = require('./contacts'),
ideas = require('./ideas'),
@@ -45,6 +46,6 @@ function deserialize(req, res, next) {
}

module.exports = {
serialize: Object.assign({ }, challenges, comments, contacts, ideas, ideaTags, messages, tags, users, votes),
serialize: Object.assign({ }, challenges, challengeTags, comments, contacts, ideas, ideaTags, messages, tags, users, votes),
deserialize
};
339 changes: 339 additions & 0 deletions test/dit-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
'use strict';

const path = require('path'),
should = require('should');

const dbHandle = require('./handle-database');
const agentFactory = require('./agent');
const models = require(path.resolve('./models'));

testDitsTags('idea');
testDitsTags('challenge');

function testDitsTags(dit){
describe(`tags of ${dit}`, () => {

let agent,
dbData,
existentDit,
loggedUser,
otherUser,
tag0,
tag1;

beforeEach(() => {
agent = agentFactory();
});

beforeEach(async () => {
const data = {
users: 3, // how many users to make
verifiedUsers: [0, 1], // which users to make verified
tags: 5,
[`${dit}s`]: [
[{ }, 0],
[{ }, 1]
],
// ditTag [0, 0] shouldn't be created here; is created in tests for POST
[`${dit}Tags`]: [[0, 1], [0, 2], [0, 3], [0, 4], [1, 0], [1, 4]]
};
// create data in database
dbData = await dbHandle.fill(data);

[loggedUser, otherUser] = dbData.users;
[existentDit] = dbData[`${dit}s`];
[tag0, tag1] = dbData.tags;
});

afterEach(async () => {
await dbHandle.clear();
});

describe(`POST /${dit}s/:id/tags`, () => {
let postBody;

beforeEach(() => {
postBody = { data: {
type: `${dit}-tags`,
relationships: {
tag: { data: { type: 'tags', id: tag0.tagname } }
}
} };
});

context(`logged as ${dit} creator`, () => {

beforeEach(() => {
agent = agentFactory.logged(loggedUser);
});

context('valid data', () => {
it(`[${dit} and tag exist and ${dit}Tag doesn't] 201`, async () => {
const response = await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(201);
const ditTagDb = await models.ditTag.read(dit, existentDit.id, tag0.tagname);
should(ditTagDb).match({
[`${dit}`]: { id: existentDit.id },
tag: { tagname: tag0.tagname },
creator: { username: loggedUser.username }
});
should(response.body).match({
data: {
type: `${dit}-tags`,
id: `${existentDit.id}--${tag0.tagname}`,
relationships: {
[`${dit}`]: { data: { type: `${dit}s`, id: existentDit.id } },
tag: { data: { type: 'tags', id: tag0.tagname } },
creator: { data: { type: 'users', id: loggedUser.username } }
}
}
});
});

it(`[duplicate ${dit}Tag] 409`, async () => {
// first it's ok
await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(201);

// duplicate request should error
await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(409);
});

it(`[${dit} doesn't exist] 404`, async () => {
const response = await agent
.post(`/${dit}s/00000000/tags`)
.send(postBody)
.expect(404);

should(response.body).deepEqual({
errors: [{
status: 404,
detail: `${dit} not found`
}]
});
});

it('[tag doesn\'t exist] 404', async () => {
// set nonexistent tag in body
postBody.data.relationships.tag.data.id = 'nonexistent-tag';

const response = await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(404);

should(response.body).deepEqual({
errors: [{
status: 404,
detail: 'tag not found'
}]
});
});

});

context('invalid data', () => {
it('[invalid id] 400', async () => {
await agent
.post(`/${dit}s/invalid-id/tags`)
.send(postBody)
.expect(400);
});

it('[invalid tagname] 400', async () => {
// invalidate tagname
postBody.data.relationships.tag.data.id = 'invalidTagname';

await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(400);
});

it('[missing tagname] 400', async () => {
// invalidate tagname
delete postBody.data.relationships.tag;

await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(400);
});

it('[additional properties in body] 400', async () => {
// add some attributes (or relationships)
postBody.data.attributes = { foo: 'bar' };

await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(400);
});
});

});

context(`logged, not ${dit} creator`, () => {
beforeEach(() => {
agent = agentFactory.logged(otherUser);
});

it('403', async () => {
const response = await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(403);

should(response.body).deepEqual({
errors: [{ status: 403, detail: `not logged in as ${dit} creator` }]
});
});
});

context('not logged', () => {
it('403', async () => {
await agent
.post(`/${dit}s/${existentDit.id}/tags`)
.send(postBody)
.expect(403);
});
});
});

describe(`GET /${dit}s/:id/tags`, () => {
context('logged', () => {

beforeEach(() => {
agent = agentFactory.logged();
});

context('valid data', () => {
it(`[${dit} exists] 200 and list of ${dit}-tags`, async () => {
const response = await agent
.get(`/${dit}s/${existentDit.id}/tags`)
.expect(200);

const responseData = response.body.data;

should(responseData).Array().length(4);
});

it(`[${dit} doesn't exist] 404`, async () => {
const response = await agent
.get(`/${dit}s/00000001/tags`)
.expect(404);

should(response.body).match({ errors: [{
status: 404,
detail: `${dit} not found`
}] });
});
});

context('invalid data', () => {
it('[invalid id] 400', async () => {
await agent
.get(`/${dit}s/invalidId/tags`)
.expect(400);
});
});

});

context('not logged', () => {
it('403', async () => {
await agent
.get(`/${dit}s/${existentDit.id}/tags`)
.expect(403);
});
});

});

describe(`DELETE /${dit}s/:id/tags/:tagname`, () => {

context(`logged as ${dit} creator`, () => {

beforeEach(() => {
agent = agentFactory.logged(loggedUser);
});

context('valid data', () => {
it(`[${dit}-tag exists] 204`, async () => {
const ditTag = await models.ditTag.read(dit, existentDit.id, tag1.tagname);

// first ditTag exists
should(ditTag).Object();

await agent
.delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`)
.expect(204);

const ditTagAfter = await models.ditTag.read(dit, existentDit.id, tag1.tagname);
// the ditTag doesn't exist
should(ditTagAfter).be.undefined();

});

it(`[${dit}-tag doesn't exist] 404`, async () => {
await agent
.delete(`/${dit}s/${existentDit.id}/tags/${tag0.tagname}`)
.expect(404);
});
});

context('invalid data', () => {
it('[invalid id] 400', async () => {
await agent
.delete(`/${dit}s/invalid-id/tags/${tag1.tagname}`)
.expect(400);
});

it('[invalid tagname] 400', async () => {
await agent
.delete(`/${dit}s/${existentDit.id}/tags/invalid--tagname`)
.expect(400);
});
});

});

context(`logged, not ${dit} creator`, () => {

beforeEach(() => {
agent = agentFactory.logged(otherUser);
});

it('403', async () => {
const response = await agent
.delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`)
.expect(403);

should(response.body).deepEqual({
errors: [{ status: 403, detail: `not logged in as ${dit} creator` }]
});
});
});

context('not logged', () => {
it('403', async () => {
const response = await agent
.delete(`/${dit}s/${existentDit.id}/tags/${tag1.tagname}`)
.expect(403);

should(response.body).not.deepEqual({
errors: [{ status: 403, detail: `not logged in as ${dit} creator` }]
});
});
});

});
});
}
1 change: 1 addition & 0 deletions test/votes.js
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ const agentFactory = require('./agent'),

voteTestFactory('idea');
voteTestFactory('comment');
voteTestFactory('challenge');

/**
* We can test votes to different objects.