diff --git a/.eslintrc b/.eslintrc index 48dda861..a5724c8d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,9 +22,14 @@ } } }, + "env": { + "jest": true, + }, "globals": { __DEV__: true, SLACK_KEY: true, SLACK_REDIRECT_URI: true, + + afterAll: true }, } diff --git a/__mocks__/console.js b/__mocks__/console.js new file mode 100644 index 00000000..b0a2ab99 --- /dev/null +++ b/__mocks__/console.js @@ -0,0 +1,2 @@ +// Mock for node-uuid +global.console.warn = () => {}; diff --git a/__mocks__/ctx.js b/__mocks__/ctx.js new file mode 100644 index 00000000..c3f66d7c --- /dev/null +++ b/__mocks__/ctx.js @@ -0,0 +1,7 @@ +const ctx = { + cache: { + set: () => {}, + }, +}; + +export default ctx; diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 00000000..08d725cd --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +export default ''; diff --git a/__mocks__/styleMock.js b/__mocks__/styleMock.js new file mode 100644 index 00000000..9775450f --- /dev/null +++ b/__mocks__/styleMock.js @@ -0,0 +1,2 @@ +import idObj from 'identity-obj-proxy'; +export default idObj; diff --git a/__mocks__/window.js b/__mocks__/window.js new file mode 100644 index 00000000..7a7b87a2 --- /dev/null +++ b/__mocks__/window.js @@ -0,0 +1 @@ +window.matchMedia = (data) => data; diff --git a/frontend/components/Alert/Alert.test.js b/frontend/components/Alert/Alert.test.js new file mode 100644 index 00000000..2bec85bc --- /dev/null +++ b/frontend/components/Alert/Alert.test.js @@ -0,0 +1,29 @@ +/* eslint-disable */ +import React from 'react'; +import { snap } from 'utils/testUtils'; + +import Alert from '.'; + +test('renders default as info', () => { + snap(default); +}); + +test('renders success', () => { + snap(success); +}); + +test('renders info', () => { + snap(info); +}); + +test('renders warning', () => { + snap(warning); +}); + +test('renders danger', () => { + snap(danger); +}); + +test('renders offline', () => { + snap(offline); +}); diff --git a/frontend/components/Alert/__snapshots__/Alert.test.js.snap b/frontend/components/Alert/__snapshots__/Alert.test.js.snap new file mode 100644 index 00000000..e419df80 --- /dev/null +++ b/frontend/components/Alert/__snapshots__/Alert.test.js.snap @@ -0,0 +1,113 @@ +exports[`test renders danger 1`] = ` +
+ danger +
+`; + +exports[`test renders default as info 1`] = ` +
+ default +
+`; + +exports[`test renders info 1`] = ` +
+ info +
+`; + +exports[`test renders offline 1`] = ` +
+ offline +
+`; + +exports[`test renders success 1`] = ` +
+ success +
+`; + +exports[`test renders warning 1`] = ` +
+ warning +
+`; diff --git a/frontend/utils/testUtils.js b/frontend/utils/testUtils.js new file mode 100644 index 00000000..9eae17b4 --- /dev/null +++ b/frontend/utils/testUtils.js @@ -0,0 +1,10 @@ +import renderer from 'react-test-renderer'; + +const snap = (children) => { + const component = renderer.create(children); + expect(component).toMatchSnapshot(); +}; + +export { + snap, +}; diff --git a/package.json b/package.json index 7fdec4c4..321b7bf6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "BeautifulAtlas", - "version": "0.0.1", - "description": "For writing", + "private": true, "main": "index.js", "scripts": { "clean": "rimraf dist", @@ -13,7 +12,29 @@ "lint": "eslint frontend", "deploy": "git push heroku master", "heroku-postbuild": "npm run build && npm run sequelize db:migrate", - "sequelize": "./node_modules/.bin/sequelize" + "sequelize": "./node_modules/.bin/sequelize", + "test": "npm run test:frontend && npm run test:server", + "test:frontend": "jest", + "test:watch": "jest --watch", + "test:server": "jest --config=server/.jest-config" + }, + "jest": { + "verbose": false, + "testPathDirs": [ + "frontend" + ], + "moduleNameMapper": { + "^.*[.](s?css|css)$": "/__mocks__/styleMock.js", + "^.*[.](gif|ttf|eot|svg)$": "/__test__/fileMock.js" + }, + "moduleFileExtensions": ["js", "jsx", "json"], + "moduleDirectories": ["node_modules", "server"], + "modulePaths": [ + "frontend" + ], + "setupFiles": [ + "/__mocks__/window.js" + ] }, "engines": { "node": "6.x" @@ -22,12 +43,6 @@ "type": "git", "url": "git+ssh://git@github.com/jorilallo/atlas.git" }, - "author": "Jori Lallo", - "license": "ISC", - "bugs": { - "url": "https://github.com/jorilallo/atlas/issues" - }, - "homepage": "https://github.com/jorilallo/atlas#readme", "dependencies": { "babel-core": "6.13.2", "babel-eslint": "6.1.2", @@ -99,10 +114,10 @@ "query-string": "^4.2.2", "querystring": "0.2.0", "randomstring": "1.1.5", - "react": "15.1.0", - "react-codemirror": "0.2.5", + "react": "15.3.1", + "react-codemirror": "0.2.6", "react-dom": "15.1.0", - "react-dropzone": "3.3.2", + "react-dropzone": "3.6.0", "react-helmet": "3.1.0", "react-keydown": "^1.6.1", "react-router": "2.5.1", @@ -125,11 +140,16 @@ "webpack": "1.13.2" }, "devDependencies": { + "babel-jest": "^15.0.0", + "fetch-test-server": "^1.1.0", "fsevents": "1.0.14", + "identity-obj-proxy": "^3.0.0", "ignore-loader": "0.1.1", + "jest-cli": "^15.1.1", "koa-webpack-dev-middleware": "1.2.0", "koa-webpack-hot-middleware": "1.0.3", "node-dev": "3.1.0", - "nodemon": "1.9.1" + "nodemon": "1.9.1", + "react-test-renderer": "^15.3.1" } } diff --git a/server/.jest-config b/server/.jest-config new file mode 100644 index 00000000..ef4c6d8f --- /dev/null +++ b/server/.jest-config @@ -0,0 +1,10 @@ +{ + "verbose": true, + "testPathDirs": [ + "/server" + ], + "setupFiles": [ + "/__mocks__/console.js", + "./server/test/helper.js" + ] +} diff --git a/server/api/__snapshots__/user.test.js.snap b/server/api/__snapshots__/user.test.js.snap new file mode 100644 index 00000000..b85f2b67 --- /dev/null +++ b/server/api/__snapshots__/user.test.js.snap @@ -0,0 +1,16 @@ +exports[`test should require authentication 1`] = ` +Object { + "message": "Authentication required" +} +`; + +exports[`test should return known user 1`] = ` +Object { + "data": Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "86fde1d4-0050-428f-9f0b-0bf77f8bdf61", + "name": "User 1", + "username": "user1" + } +} +`; diff --git a/server/api/middlewares/authentication.js b/server/api/middlewares/authentication.js index ba797357..b1efbaa4 100644 --- a/server/api/middlewares/authentication.js +++ b/server/api/middlewares/authentication.js @@ -10,8 +10,6 @@ export default function auth({ require = true } = {}) { return async function authMiddleware(ctx, next) { let token; - console.log(ctx.body); - const authorizationHeader = ctx.request.get('authorization'); if (authorizationHeader) { const parts = authorizationHeader.split(' '); diff --git a/server/api/user.test.js b/server/api/user.test.js new file mode 100644 index 00000000..f08bc6cc --- /dev/null +++ b/server/api/user.test.js @@ -0,0 +1,37 @@ +import TestServer from 'fetch-test-server'; + +import app from '..'; +import { User } from '../models'; + +import { flushdb, seed, sequelize } from '../test/support'; + +const server = new TestServer(app.callback()); + +beforeEach(seed); +afterEach(flushdb); +afterAll(() => server.close()); +afterAll(() => sequelize.close()); + +it('should return known user', async () => { + const user = await User.findOne({ + where: { + email: 'user1@example.com', + }, + }); + + const res = await server.post('/api/user.info', { + body: { token: user.getJwtToken() }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body).toMatchSnapshot(); +}); + +it('should require authentication', async () => { + const res = await server.post('/api/user.info'); + const body = await res.json(); + + expect(res.status).toEqual(401); + expect(body).toMatchSnapshot(); +}); diff --git a/server/models/User.js b/server/models/User.js index a7a0ba42..896636f8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -2,7 +2,7 @@ import crypto from 'crypto'; import { DataTypes, sequelize, - encryptedFields + encryptedFields, } from '../sequelize'; import JWT from 'jsonwebtoken'; diff --git a/server/presenters.js b/server/presenters.js index d5d24705..f7ae67a4 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -1,5 +1,5 @@ import Sequelize from 'sequelize'; -import _orderBy from 'lodash.orderby'; +import _ from 'lodash'; import { Document, Atlas, User, Revision } from './models'; export function presentUser(ctx, user) { @@ -123,7 +123,7 @@ export function presentCollection(ctx, collection, includeRecentDocuments=false) includeCollaborators: true, })); })); - data.recentDocuments = _orderBy(recentDocuments, ['updatedAt'], ['desc']); + data.recentDocuments = _.orderBy(recentDocuments, ['updatedAt'], ['desc']); } resolve(data); diff --git a/server/presenters/__snapshots__/user.test.js.snap b/server/presenters/__snapshots__/user.test.js.snap new file mode 100644 index 00000000..eb254de7 --- /dev/null +++ b/server/presenters/__snapshots__/user.test.js.snap @@ -0,0 +1,8 @@ +exports[`test presents a user 1`] = ` +Object { + "avatarUrl": "http://example.com/avatar.png", + "id": "123", + "name": "Test User", + "username": "testuser" +} +`; diff --git a/server/presenters/user.js b/server/presenters/user.js new file mode 100644 index 00000000..9c22daac --- /dev/null +++ b/server/presenters/user.js @@ -0,0 +1,15 @@ +const presentUser = (ctx, user) => { + ctx.cache.set(user.id, user); + + return new Promise(async (resolve, _reject) => { + const data = { + id: user.id, + name: user.name, + username: user.username, + avatarUrl: user.slackData.image_192, + }; + resolve(data); + }); +}; + +export default presentUser; diff --git a/server/presenters/user.test.js b/server/presenters/user.test.js new file mode 100644 index 00000000..87967021 --- /dev/null +++ b/server/presenters/user.test.js @@ -0,0 +1,19 @@ +import presentUser from './user'; + +import ctx from '../../__mocks__/ctx'; + +it('presents a user', async () => { + const user = await presentUser( + ctx, + { + id: '123', + name: 'Test User', + username: 'testuser', + slackData: { + image_192: 'http://example.com/avatar.png', + }, + }, + ); + + expect(user).toMatchSnapshot(); +}); diff --git a/server/test/helper.js b/server/test/helper.js new file mode 100644 index 00000000..f37bc82b --- /dev/null +++ b/server/test/helper.js @@ -0,0 +1,30 @@ +require('localenv'); + +// test environment variables +process.env.DATABASE_URL = process.env.DATABASE_URL_TEST; +process.env.NODE_ENV = 'test'; + +const Sequelize = require('sequelize'); +const sequelize = require('../sequelize').sequelize; +const Umzug = require('umzug'); + +const queryInterface = sequelize.getQueryInterface(); + +function runMigrations() { + const umzug = new Umzug({ + storage: 'sequelize', + storageOptions: { + sequelize, + }, + migrations: { + params: [queryInterface, Sequelize], + path: './server/migrations', + }, + }); + umzug.up() + .then(() => { + sequelize.close(); + }); +} + +runMigrations(); diff --git a/server/test/support.js b/server/test/support.js new file mode 100644 index 00000000..4a379484 --- /dev/null +++ b/server/test/support.js @@ -0,0 +1,29 @@ +import { User } from '../models'; +import { sequelize } from '../sequelize'; + +export function flushdb() { + const sql = sequelize.getQueryInterface(); + const tables = Object.keys(sequelize.models).map((model) => + sql.quoteTable(sequelize.models[model].getTableName())); + const query = `TRUNCATE ${tables.join(', ')} CASCADE`; + + return sequelize.query(query); +} + +const seed = async () => { + await User.create({ + id: '86fde1d4-0050-428f-9f0b-0bf77f8bdf61', + email: 'user1@example.com', + username: 'user1', + name: 'User 1', + slackId: '123', + slackData: { + image_192: 'http://example.com/avatar.png', + }, + }); +}; + +export { + seed, + sequelize, +};