diff --git a/server/api/__snapshots__/auth.test.js.snap b/server/api/__snapshots__/auth.test.js.snap new file mode 100644 index 00000000..f921db9f --- /dev/null +++ b/server/api/__snapshots__/auth.test.js.snap @@ -0,0 +1,27 @@ +exports[`test should require params 1`] = ` +Object { + "error": "name is required", + "ok": false +} +`; + +exports[`test should require unique email 1`] = ` +Object { + "error": "User already exists with this email", + "ok": false +} +`; + +exports[`test should require unique username 1`] = ` +Object { + "error": "User already exists with this username", + "ok": false +} +`; + +exports[`test should require valid email 1`] = ` +Object { + "error": "email is invalid", + "ok": false +} +`; diff --git a/server/api/auth.js b/server/api/auth.js index 351331ad..6783bd7b 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -8,6 +8,36 @@ import { User, Team } from '../models'; const router = new Router(); +router.post('auth.signup', async (ctx) => { + const { username, name, email, password } = ctx.request.body; + + ctx.assertPresent(username, 'name is required'); + ctx.assertPresent(name, 'name is required'); + ctx.assertPresent(email, 'email is required'); + ctx.assertEmail(email, 'email is invalid'); + ctx.assertPresent(password, 'password is required'); + + if (await User.findOne({ where: { email } })) { + throw httpErrors.BadRequest('User already exists with this email'); + } + + if (await User.findOne({ where: { username } })) { + throw httpErrors.BadRequest('User already exists with this username'); + } + + const user = await User.create({ + username, + name, + email, + password, + }); + + ctx.body = { data: { + user: await presentUser(ctx, user), + accessToken: user.getJwtToken(), + } }; +}); + router.post('auth.slack', async (ctx) => { const { code } = ctx.body; ctx.assertPresent(code, 'code is required'); diff --git a/server/api/auth.test.js b/server/api/auth.test.js new file mode 100644 index 00000000..03fa35b1 --- /dev/null +++ b/server/api/auth.test.js @@ -0,0 +1,85 @@ +import TestServer from 'fetch-test-server'; +import app from '..'; +import { flushdb, sequelize, seed } from '../test/support'; + +const server = new TestServer(app.callback()); + +beforeEach(flushdb); +afterAll(() => server.close()); +afterAll(() => sequelize.close()); + +it('should signup a new user', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'new.user@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.ok).toBe(true); + expect(body.data.user).toBeTruthy(); +}); + +it('should require params', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + + +it('should require valid email', async () => { + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + +it('should require unique email', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'testuser', + name: 'Test User', + email: 'user1@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); + +it('should require unique username', async () => { + await seed(); + const res = await server.post('/api/auth.signup', { + body: { + username: 'user1', + name: 'Test User', + email: 'userone@example.com', + password: 'test123!', + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(400); + expect(body).toMatchSnapshot(); +}); diff --git a/server/migrations/20160911230444-user-optional-slack-id.js b/server/migrations/20160911230444-user-optional-slack-id.js new file mode 100644 index 00000000..b1bf1f18 --- /dev/null +++ b/server/migrations/20160911230444-user-optional-slack-id.js @@ -0,0 +1,46 @@ +/* eslint-disable */ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'slackId', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + queryInterface.changeColumn( + 'teams', + 'slackId', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'slackId', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + queryInterface.changeColumn( + 'teams', + 'slackId', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + }, +}; diff --git a/server/migrations/20160911232911-user-unique-fields.js b/server/migrations/20160911232911-user-unique-fields.js new file mode 100644 index 00000000..d394960f --- /dev/null +++ b/server/migrations/20160911232911-user-unique-fields.js @@ -0,0 +1,46 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'email', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + queryInterface.changeColumn( + 'users', + 'username', + { + type: Sequelize.STRING, + unique: true, + allowNull: false, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.changeColumn( + 'users', + 'email', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + + queryInterface.changeColumn( + 'users', + 'username', + { + type: Sequelize.STRING, + unique: false, + allowNull: true, + } + ); + } +}; diff --git a/server/models/Team.js b/server/models/Team.js index 275fd2ef..eda54bfc 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -9,7 +9,7 @@ import User from './User'; const Team = sequelize.define('team', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, name: DataTypes.STRING, - slackId: { type: DataTypes.STRING, unique: true }, + slackId: { type: DataTypes.STRING, allowNull: true }, slackData: DataTypes.JSONB, }, { instanceMethods: { diff --git a/server/models/User.js b/server/models/User.js index 896636f8..4968e9a3 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -9,12 +9,12 @@ import JWT from 'jsonwebtoken'; const User = sequelize.define('user', { id: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, primaryKey: true }, - email: DataTypes.STRING, - username: DataTypes.STRING, + email: { type: DataTypes.STRING, unique: true }, + username: { type: DataTypes.STRING, unique: true }, name: DataTypes.STRING, isAdmin: DataTypes.BOOLEAN, slackAccessToken: encryptedFields.vault('slackAccessToken'), - slackId: { type: DataTypes.STRING, unique: true }, + slackId: { type: DataTypes.STRING, allowNull: true }, slackData: DataTypes.JSONB, jwtSecret: encryptedFields.vault('jwtSecret'), }, { diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap index eb254de7..26af224b 100644 --- a/server/presenters/__snapshots__/user.test.js.snap +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -6,3 +6,12 @@ Object { "username": "testuser" } `; + +exports[`test presents a user without slack data 1`] = ` +Object { + "avatarUrl": null, + "id": "123", + "name": "Test User", + "username": "testuser" +} +`; diff --git a/server/presenters/user.js b/server/presenters/user.js index 9c22daac..b8d997d9 100644 --- a/server/presenters/user.js +++ b/server/presenters/user.js @@ -4,9 +4,9 @@ const presentUser = (ctx, user) => { return new Promise(async (resolve, _reject) => { const data = { id: user.id, - name: user.name, username: user.username, - avatarUrl: user.slackData.image_192, + name: user.name, + avatarUrl: user.slackData ? user.slackData.image_192 : null, }; resolve(data); }); diff --git a/server/presenters/user.test.js b/server/presenters/user.test.js index 87967021..a26c10fd 100644 --- a/server/presenters/user.test.js +++ b/server/presenters/user.test.js @@ -17,3 +17,17 @@ it('presents a user', async () => { expect(user).toMatchSnapshot(); }); + +it('presents a user without slack data', async () => { + const user = await presentUser( + ctx, + { + id: '123', + name: 'Test User', + username: 'testuser', + slackData: null, + }, + ); + + expect(user).toMatchSnapshot(); +}); diff --git a/server/test/helper.js b/server/test/helper.js index f37bc82b..2823ad67 100644 --- a/server/test/helper.js +++ b/server/test/helper.js @@ -21,9 +21,9 @@ function runMigrations() { path: './server/migrations', }, }); - umzug.up() + return umzug.up() .then(() => { - sequelize.close(); + return sequelize.close(); }); }