Skip to content
This repository has been archived by the owner on Mar 7, 2023. It is now read-only.

Commit

Permalink
Merge pull request #489 from sgmap/641-profile-snapshot-api
Browse files Browse the repository at this point in the history
[#489] [FEATURE] création d'un instantané du profil de compétence (Api) (US-641)
  • Loading branch information
florianEnoh committed Aug 24, 2017
2 parents 9b54583 + 807e9d1 commit 7d6770c
Show file tree
Hide file tree
Showing 18 changed files with 1,147 additions and 2 deletions.
28 changes: 28 additions & 0 deletions api/db/migrations/20170818115953_snapshots.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const TABLE_NAME = 'snapshots';

exports.up = function(knex) {

function table(t) {
t.increments().primary();
t.string('organizationId').unsigned().references('organizations.id');
t.string('userId').unsigned().references('users.id');
t.string('score');
t.json('profile').notNullable();
t.dateTime('createdAt').notNullable().defaultTo(knex.fn.now());
t.dateTime('updatedAt').notNullable().defaultTo(knex.fn.now());
}

return knex.schema
.createTable(TABLE_NAME, table)
.then(() => {
console.log(`${TABLE_NAME} table is created!`);
});
};

exports.down = function(knex) {
return knex.schema
.dropTable(TABLE_NAME)
.then(() => {
console.log(`${TABLE_NAME} table was dropped!`);
});
};
20 changes: 20 additions & 0 deletions api/lib/application/snapshots/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const snapshotController = require('./snapshot-controller');
exports.register = function(server, options, next) {

server.route([
{
method: 'POST',
path: '/api/snapshots',
config: {
handler: snapshotController.create, tags: ['api']
}
}
]);

return next();
};

exports.register.attributes = {
name: 'snapshots-api',
version: '1.0.0'
};
78 changes: 78 additions & 0 deletions api/lib/application/snapshots/snapshot-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const authorizationToken = require('../../../lib/infrastructure/validators/jsonwebtoken-verify');
const validationErrorSerializer = require('../../infrastructure/serializers/jsonapi/validation-error-serializer');
const UserRepository = require('../../../lib/infrastructure/repositories/user-repository');
const OrganizationRepository = require('../../../lib/infrastructure/repositories/organization-repository');
const snapshotSerializer = require('../../../lib/infrastructure/serializers/jsonapi/snapshot-serializer');
const profileSerializer = require('../../../lib/infrastructure/serializers/jsonapi/profile-serializer');
const SnapshotService = require('../../../lib/domain/services/snapshot-service');
const profileService = require('../../domain/services/profile-service');
const logger = require('../../../lib/infrastructure/logger');
const { InvalidTokenError, NotFoundError, InvaliOrganizationIdError } = require('../../domain/errors');

function _assertThatOrganizationExists(organizationId) {
return OrganizationRepository.isOrganizationIdExist(organizationId)
.then((isOrganizationExist) => {
if(!isOrganizationExist) {
throw new InvaliOrganizationIdError();
}
});
}

const _replyErrorWithMessage = function(reply, errorMessage, statusCode) {
reply(validationErrorSerializer.serialize(_handleWhenInvalidAuthorization(errorMessage))).code(statusCode);
};

function _handleWhenInvalidAuthorization(errorMessage) {
return {
data: {
authorization: [errorMessage]
}
};
}

function _extractOrganizationId(request) {
return request.hasOwnProperty('payload') && request.payload.data && request.payload.data.attributes['organization-id'] || '';
}

function _hasAnAtuhorizationHeaders(request) {
return request && request.hasOwnProperty('headers') && request.headers.hasOwnProperty('authorization');
}

function _replyError(err, reply) {
if(err instanceof InvalidTokenError) {
return _replyErrorWithMessage(reply, 'Le token n’est pas valide', 401);
}

if(err instanceof NotFoundError) {
return _replyErrorWithMessage(reply, 'Cet utilisateur est introuvable', 422);
}

if(err instanceof InvaliOrganizationIdError) {
return _replyErrorWithMessage(reply, 'Cette organisation n’existe pas', 422);
}
logger.error(err);
return _replyErrorWithMessage(reply, 'Une erreur est survenue lors de la création de l’instantané', 500);
}

function create(request, reply) {

if(!_hasAnAtuhorizationHeaders(request)) {
return _replyErrorWithMessage(reply, 'Le token n’est pas valide', 401);
}

const token = request.headers.authorization;
const organizationId = _extractOrganizationId(request);

return authorizationToken
.verify(token)
.then(UserRepository.findUserById)
.then((foundUser) => _assertThatOrganizationExists(organizationId).then(() => foundUser))
.then(({ id }) => profileService.getByUserId(id))
.then((profile) => profileSerializer.serialize(profile))
.then((profile) => SnapshotService.create({ organizationId, profile }))
.then((snapshotId) => snapshotSerializer.serialize({ id: snapshotId }))
.then(snapshotSerialized => reply(snapshotSerialized).code(201))
.catch((err) => _replyError(err, reply));
}

module.exports = { create };
15 changes: 14 additions & 1 deletion api/lib/domain/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ class NotFoundError extends Error {
}
}

class InvaliOrganizationIdError extends Error {
constructor(message) {
super(message);
}
}

class InvalidTokenError extends Error {
constructor(message) {
super(message);
Expand All @@ -28,4 +34,11 @@ class AlreadyRegisteredEmailError extends Error {
}
}

module.exports = { NotFoundError, NotElligibleToScoringError, PasswordNotMatching, InvalidTokenError, AlreadyRegisteredEmailError };
module.exports = {
NotFoundError,
NotElligibleToScoringError,
PasswordNotMatching,
InvalidTokenError,
AlreadyRegisteredEmailError,
InvaliOrganizationIdError
};
15 changes: 15 additions & 0 deletions api/lib/domain/models/data/snapshot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const Bookshelf = require('../../../infrastructure/bookshelf');
const Organization = require('./organization');
const User = require('./user');

module.exports = Bookshelf.Model.extend({
tableName: 'snapshots',

organizations() {
return this.belongsTo(Organization);
},

users() {
return this.belongsTo(User);
}
});
16 changes: 16 additions & 0 deletions api/lib/domain/services/snapshot-service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const snapshotRepository = require('../../../lib/infrastructure/repositories/snapshot-repository');

module.exports = {
create(snapshot) {
const snapshotRaw = {
organizationId: snapshot.organizationId,
userId: snapshot.profile.data.id,
score: snapshot.profile.data.attributes['total-pix-score'],
profile: JSON.stringify(snapshot.profile)
};

return snapshotRepository
.save(snapshotRaw)
.then((snapshot) => snapshot.get('id'));
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ module.exports = {
});
},

isOrganizationIdExist(id) {
return Organization
.where({ id })
.fetch()
.then(organizations => !!organizations);
},

get(id) {
return Organization
.where({ id: id })
Expand Down
7 changes: 7 additions & 0 deletions api/lib/infrastructure/repositories/snapshot-repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const Snapshot = require('../../domain/models/data/snapshot');

module.exports = {
save(snapshotRawData) {
return new Snapshot(snapshotRawData).save();
}
};
15 changes: 15 additions & 0 deletions api/lib/infrastructure/serializers/jsonapi/snapshot-serializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const JSONAPISerializer = require('jsonapi-serializer').Serializer;

class SnapshotSerializer {
serialize(snapshot) {
return new JSONAPISerializer('snapshots', {
attributes: ['id'],
transform(snapshot) {
snapshot.id = snapshot.id.toString();
return snapshot;
}
}).serialize(snapshot);
}
}

module.exports = new SnapshotSerializer();
3 changes: 2 additions & 1 deletion api/lib/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = [
require('./application/users'),
require('./application/authentication'),
require('./application/cache'),
require('./application/organizations')
require('./application/organizations'),
require('./application/snapshots')
];
136 changes: 136 additions & 0 deletions api/tests/acceptance/application/snapshot-controller_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const faker = require('faker');
const bcrypt = require('bcrypt');
const { describe, it, after, before, expect, afterEach, beforeEach, knex, sinon } = require('../../test-helper');
const authorizationToken = require('../../../lib/infrastructure/validators/jsonwebtoken-verify');
const profileService = require('../../../lib/domain/services/profile-service');
const User = require('../../../lib/domain/models/data/user');
const server = require('../../../server');

describe('Acceptance | Controller | snapshot-controller', function() {

let userId;
let organizationId;
const userPassword = bcrypt.hashSync('A124B2C3#!', 1);
const fakeUser = new User({
id: 'user_id',
'firstName': faker.name.firstName(),
'lastName': faker.name.lastName(),
'email': faker.internet.email()
});
const fakeBuildedProfile = {
user: fakeUser,
competences: [{
id: 'recCompA',
name: 'competence-name-1',
index: '1.1',
areaId: 'recAreaA',
level: -1,
courseId: 'recBxPAuEPlTgt72q11'
},
{
id: 'recCompB',
name: 'competence-name-2',
index: '1.2',
areaId: 'recAreaB',
level: -1,
courseId: 'recBxPAuEPlTgt72q99'
}],
areas: [{ id: 'recAreaA', name: 'domaine-name-1' }, { id: 'recAreaB', name: 'domaine-name-2' }],
organizations: []
};

const inserted_user = {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
email: faker.internet.email(),
password: userPassword,
cgu: true
};

const inserted_organization = {
name: 'The name of the organization',
email: 'organization@email.com',
type: 'PRO'
};

before(() => {
return knex.migrate.latest()
.then(() => {
return knex.seed.run();
}).then(() => {
return knex('users').insert(inserted_user);
}).then((result) => {
userId = result.shift();
inserted_organization['userId'] = userId;
return knex('organizations').insert(inserted_organization);
}).then((organization) => {
organizationId = organization.shift();
});
});

after(function(done) {
server.stop(done);
});

describe('POST /api/snapshots', function() {

let payload;
let options;
let injectPromise;

beforeEach(() => {
payload = {
data: {
attributes: {
'organization-id': organizationId
}
}
};

options = {
method: 'POST',
url: '/api/snapshots',
payload
};

options['headers'] = { authorization: 'VALID_TOKEN' };
sinon.stub(authorizationToken, 'verify').resolves(userId);
sinon.stub(profileService, 'getByUserId').resolves(fakeBuildedProfile);
injectPromise = server.inject(options);
});

afterEach(() => {
authorizationToken.verify.restore();
profileService.getByUserId.restore();
return knex('snapshots').delete();
});

it('should return 201 HTTP status code', () => {
// When
return injectPromise.then((response) => {
// then
expect(response.statusCode).to.equal(201);
expect(response.result.data.id).to.exist;
});
});

describe('when creating with a wrong payload', () => {

it('should return 422 HTTP status code', () => {
// Given
payload.data.attributes['organization-id'] = null;

// Then
const creatingSnapshotWithWrongParams = server.inject(options);

// Then
return creatingSnapshotWithWrongParams.then((response) => {
const parsedResponse = JSON.parse(response.payload);
expect(parsedResponse.errors[0].detail).to.equal('Cette organisation n’existe pas');
expect(response.statusCode).to.equal(422);
});
});

});
});
});
Loading

0 comments on commit 7d6770c

Please sign in to comment.