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,
+};