diff --git a/.babelrc b/.babelrc
old mode 100644
new mode 100755
index c6c9efb..57c2d42
--- a/.babelrc
+++ b/.babelrc
@@ -1,3 +1,4 @@
{
-"presets": ["es2015"]
+"presets": ["env"],
+"plugins": ["transform-es2015-destructuring", "transform-object-rest-spread"]
}
\ No newline at end of file
diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100755
index 0000000..78923d9
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,17 @@
+engines:
+ duplication:
+ enabled: false
+ config:
+ languages:
+ javascript:
+ mass_threshold: 20
+ratings:
+ paths:
+ - "**.js"
+exclude_paths:
+- "server/src/api/v1/tests/"
+- "node_modules/"
+- "template/"
+- "server/dist/"
+- "client/node_modules/"
+- "server/src/docs/"
diff --git a/.coveralls.yml b/.coveralls.yml
index 2d8a2e0..6d10cd2 100644
--- a/.coveralls.yml
+++ b/.coveralls.yml
@@ -1 +1 @@
-repo_token: COVERALLS_TOKEN
\ No newline at end of file
+repo_token: COVERALLS_REPO_TOKEN
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
old mode 100644
new mode 100755
diff --git a/.env.sample b/.env.sample
new file mode 100644
index 0000000..6c9ac4e
--- /dev/null
+++ b/.env.sample
@@ -0,0 +1,24 @@
+COVERALLS_REPO_TOKEN=
+
+TEST_DB_NAME=
+TEST_DB_HOST=
+TEST_DB_PASSWORD=
+TEST_DB_USERNAME=
+
+PRODUCTION_DB_NAME=
+PRODUCTION_DB_HOST=
+PRODUCTION_DB_PASSWORD=
+PRODUCTION_DB_USERNAME=
+
+DEVELOPMENT_DB_NAME=
+DEVELOPMENT_DB_HOST=
+DEVELOPMENT_DB_PASSWORD=
+DEVELOPMENT_DB_USERNAME=
+
+TOKEN_SECRET=
+
+GMAIL_USERNAME=
+GMAIL_PASSWORD=
+
+EMAIL_FROM=
+TIMEZONE=
\ No newline at end of file
diff --git a/.eslintignore b/.eslintignore
old mode 100644
new mode 100755
index 650380a..8b1f3ea
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,10 @@
-node_modules/
-client/
template/
-template/assets/js/**
+template/assets/**
server/dist/**
.idea/
+template/**
+coverage/
+.nyc_output/
+node_modules
+.env
+server/src/docs
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
old mode 100644
new mode 100755
index d7d5761..4d650cd
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,33 +1,36 @@
-{
- "root": true,
- "extends": "airbnb-base",
- "env": {
- "node": true,
- "es6": true,
- "mocha": true
- },
- "rules": {
- "one-var": 0,
- "one-var-declaration-per-line": 0,
- "new-cap": 0,
- "consistent-return": 0,
- "no-param-reassign": 0,
- "comma-dangle": 0,
- "curly": ["error", "multi-line"],
- "import/no-unresolved": [2, { "commonjs": true }],
- "no-shadow": ["error", { "allow": ["req", "res", "err"] }],
- "valid-jsdoc": ["error", {
- "requireReturn": true,
- "requireReturnType": true,
- "requireParamDescription": false,
- "requireReturnDescription": true
- }],
- "require-jsdoc": ["error", {
- "require": {
- "FunctionDeclaration": true,
- "MethodDefinition": true,
- "ClassDeclaration": true
- }
- }]
- }
-}
\ No newline at end of file
+{
+ "root": true,
+ "extends": [ "airbnb-base" ],
+ "env": {
+ "node": true,
+ "es6": true,
+ "mocha": true
+ },
+ "rules": {
+ "max-len": ["error", 80],
+ "one-var": 0,
+ "one-var-declaration-per-line": 0,
+ "new-cap": 0,
+ "consistent-return": 0,
+ "no-param-reassign": 0,
+ "comma-dangle": 0,
+ "curly": ["error", "multi-line"],
+ "import/no-unresolved": [2, { "commonjs": true }],
+ "no-shadow": ["error", { "allow": ["req", "res", "err"] }],
+ "valid-jsdoc": ["error", {
+ "requireReturn": true,
+ "requireReturnType": true,
+ "requireParamDescription": false,
+ "requireReturnDescription": true,
+ "matchDescription": ".+"
+ }],
+ "require-jsdoc": ["error", {
+ "require": {
+ "FunctionDeclaration": true,
+ "MethodDefinition": true,
+ "ClassDeclaration": true,
+ "ArrowFunctionExpression": true
+ }
+ }]
+ }
+ }
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
old mode 100644
new mode 100755
index 0888f89..5f98d24
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,12 @@
node_modules/
-server/src/config.json
+**/node_modules/
.env
.idea/
-server/dist/**/*.js
-!server/dist/config
\ No newline at end of file
+client/src/assets/css/*.css
+server/dist/**
+coverage/
+.nyc_output/
+.DS_STORE
+client/build
+.vscode
+reports
diff --git a/.hound.yml b/.hound.yml
old mode 100644
new mode 100755
index 624a9b2..9ad3511
--- a/.hound.yml
+++ b/.hound.yml
@@ -1,4 +1,4 @@
-eslint:
- enabled: true
- config_file: .eslintrc
+eslint:
+ enabled: true
+ config_file: .eslintrc
ignore_file: .eslintignore
\ No newline at end of file
diff --git a/.istanbul.yml b/.istanbul.yml
new file mode 100644
index 0000000..1b327f9
--- /dev/null
+++ b/.istanbul.yml
@@ -0,0 +1,2 @@
+instrumentation:
+ root: src
\ No newline at end of file
diff --git a/.sequelizerc b/.sequelizerc
old mode 100644
new mode 100755
index 08c2859..8b9a5bf
--- a/.sequelizerc
+++ b/.sequelizerc
@@ -1,9 +1,9 @@
const path = require('path');
-
+const version = 'v1';
module.exports = {
- "config": path.resolve('./server/dist/config', 'config.json'),
- "models-path": path.resolve('./server/src/models'),
- "seeders-path": path.resolve('./server/src/seeders'),
- "migrations-path": path.resolve('./server/src/migrations')
+ "config": path.resolve('./server/dist/config', 'index.js'),
+ "models-path": path.resolve(`./server/dist/api/${version}/models`),
+ "seeders-path": path.resolve(`./server/dist/api/${version}/seeders`),
+ "migrations-path": path.resolve(`./server/dist/api/${version}/migrations`)
};
diff --git a/.travis.yml b/.travis.yml
old mode 100644
new mode 100755
index aa9877a..47feb54
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,18 +1,35 @@
+env:
+ global:
+ - CC_TEST_REPORTER_ID=$CODECLIMATE_REPO_TOKEN
+ - GIT_COMMITTED_AT=$(if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then git log -1 --pretty=format:%ct; else git log -1 --skip 1 --pretty=format:%ct; fi)
language: node_js
+before_script:
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
+ - chmod +x ./cc-test-reporter
+script:
+ - bundle exec rspec
+ # Preferably you will run test-reporter on branch update events. But
+ # if you setup travis to build PR updates only, you don't need to run
+ # the line below
+ - if [ "$TRAVIS_PULL_REQUEST" == "false" ]; then ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT; fi
+ # In the case where travis is setup to build PR updates only,
+ # uncomment the line below
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
node_js:
- node
notifications:
- - email: false
+ email: false
+ webhooks: https://coveralls.io/webhook?repo_token=COVERALLS_REPO_TOKEN
services:
- postgresql
before_script:
- - npm install -g codeclimate-test-reporter
- - npm install -g sequelize
+ - npm install -g codeclimate-test-reporter coveralls sequelize jest istanbul-combine
- psql -c 'drop database if exists travis;' -U postgres
- psql -c 'create database travis;' -U postgres
- npm run build
- - NODE_ENV=test npm run test:db
script:
- - npm test
+ - npm run test:coverage
after_success:
- - CODECLIMATE_REPO_TOKEN=CODECLIMATE_TOKEN codeclimate-test-reporter < coverage/lcov.info
\ No newline at end of file
+ - istanbul-combine -d merged-coverage -p summary -r lcov client/coverage/coverage-*.json coverage/coverage-*.json
+ - codeclimate-test-reporter < merged-coverage/lcov.info
+ - coveralls < merged-coverage/lcov.info
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..435d689
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2017 Fadojutimi Temitayo Olusegun
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/Procfile b/Procfile
old mode 100644
new mode 100755
index e8f79ea..3835a2e
--- a/Procfile
+++ b/Procfile
@@ -1 +1,2 @@
-web: npm start
\ No newline at end of file
+web: npm run start
+clock: node server/dist/api/v1/cron/index.js
\ No newline at end of file
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 8513b03..08cb0d3
--- a/README.md
+++ b/README.md
@@ -1,63 +1,125 @@
-# HELLO-BOOKS [](https://travis-ci.org/adesege/HelloBooks) [](https://coveralls.io/github/adesege/HelloBooks?branch=development) [](https://codeclimate.com/github/adesege/HelloBooks)
+[](https://travis-ci.org/adesege/HelloBooks)
+[](https://codeclimate.com/github/adesege/HelloBooks)
+[](https://codeclimate.com/github/adesege/HelloBooks/coverage)
+[](https://coveralls.io/github/adesege/HelloBooks?branch=chore/153550278/feedback-implementation)
+[](https://codeclimate.com/github/adesege/HelloBooks)
+# Hello-Books
A simple application that helps manage a library and its processes like stocking, tracking and renting books.
-Built on `Javascript` with `Postgres` as database.
+Built on `Nodejs`.
-## HOW TO INSTALL
-### PRE-REQUISITES
+
+
+
+## How to install
+### Pre-requisites
You will need to have the following installed in your working environment before this application can work.
* Latest version of Nodejs - comes with a Node Package Manager
* Postgresql
-### INSTALLING
+### Installing
1. Download or clone this branch at https://github.com/adesege/HelloBooks.git
2. Install dependencies by running `npm install`. Ensure you are in your working directory. Run `cd /path/to/HelloBooks` to change.
-3. Start the server by running `npm run watch`.
-
-The application listens on port `8080` by default unless otherwise started in the environment port.
-
-Visit `http://localhost:8000` to access the front end or `http://localhost:8000/api` to access the `api` endpoint.
-
-## ENDPOINTS
-The endpoints are listed below.
-Where `:versionNumber` is the API version number.
-`/api/:versionNumber`
-1. `/users`
- * *POST* /signup - Creates a user
- * *POST* /signin - Logs a user in and generates a `JSON Web Token`
- * *POST* /:userId/books - Allow users borrow a book.
- * *PUT* /:userId/books - Allow users modfy a book.
- * *GET* /:userId/books?returned=false - Allow users get all the books they have borrowed but are yet to return
-2. `/books`
- * *POST* / - Allow users add a book
- * *PUT* /:bookId - Allow users modify a book information
- * *GET* / - Allow users get all the books they have borrowed but are yet to return
- 2. `/books/stocks`
- * *POST* / - Allow admin add a stock
- * *DELETE* - Allow admin delete a book
- * *GET* / - Allow admin get stock
-
-## JSON WEB TOKEN
-This application uses JSON web token to sign and verify user token. The default expiration time is `24 hours` but this can be modified in the application config.
-
-## MIDDLEWARES
+1. Go to client directory and run `npm run install` to install dependencies.
+3. Go to the root dir `cd ../`
+1. Then type `npm run transpile` to transpile from es6 to es5
+1. and then run `npm run start:dev` in your terminal to start the server.
+1. To start the client, `cd client`
+1. Then `npm run start`
+
+The server and client listens on port `5000` and `3000` by default respectively unless otherwise stated as an environment variable.
+
+Visit `http://localhost:3000` to access the front end and `http://localhost:5000/api` to access the `api` endpoint.
+
+## Authentication Mechanism
+This application uses JSON web token to sign and verify users. The default expiration time is `24 hours` but this can be modified in the application config.
+
+## Middlewares
Some endpoints are restricted to logged users and admins only. E.g. Only admin can access `api/:versionNumber/books/stocks`.
-There are three middlewares defined in this application.
-* Authenticate middleware - verifies a user's token and checks if the user is valid.
-* userAuthenticate middleware - verifies if the user is `user`
-* adminAuthenticate middleware - checks if the user is `admin`.
+There are two middlewares defined in this application.
+* authMiddleware - verifies a user's token and checks if the user is valid.
+* adminMiddleware - checks if the user is `admin`.
+
+## Documentation
+> If Ruby is already installed, but the bundle command doesn't work, just run `gem install bundler` in a terminal.
+### Starting the server
+1. `cd slate` from the root directory.
+1. Initialize and start Slate. You can either do this locally, or with Vagrant:
+```js
+ npm run slate:install
+ # then
+ npm run slate:build
+ # finally
+ npm run slate:start
+ # OR run this to run with vagrant
+ vagrant up
+```
+
+Please visit the application documentation at http://hellobooks.herokuapp.com/docs/v1 or http://localhost:4567 to test locally
+
+To deploy generated files into your application, run `npm run slate:build`
+
+# Testing
+There are three different kind of testing in this application; client, server and end-to-end testing.
+
+> To start the server test. In your CMD, run
+```js
+ npm run test
+
+ # To get coverage result, run
+ npm run test:coverage
+```
+
+> To start the end-to-end test
+```js
+# For the first time you are running it,
+# you'll need to install the selenium server and chrome driver.
+# To do that, run
+npm run test:e2e-install
+
+# then start the server with
+npm run test:e2e-server
+
+# finally, run the test with
+npm run test:e2e
+```
+
+> Ensure that the server and client are up and running. The client must listen to port 3000. Otherwise, change it in [nightwatch.conf:16](./nightwatch.json#L16)
+
+> Finally, for the client test,
+```
+# first cd to the test folder
+# then run
+npm run test
+
+# for coverage report, run
+npm run test:coverage
+```
+
+# Author
+
+**Temitayo Fadojutimi** is a Software Developer at Andela and he dedicates his expertise to solving practical problems in the society. He tweets at [@adesege_](http://twitter.com/adesege_)
-## RESPONSE STATUSES
-These are the common status codes used in the app.
+# Contributing Guide
-1. `400 Bad Request` - Used when there is a validation error.
-2. `500 Internal Server Error` - A generic error message, given when no more specific message is suitable.
-3. `404 Not Found` - Used when the application returns an empty result.
-4. `200 OK` - All is well and set.
-5. `401 Unauthorized` - Used when authentication failed.
-6. `403 Forbidden` - Used when a user is accessing a restricted end point.
-7. `201 Created` - Used when a new record is inserted into the database.
+Thank you for your interest in contributing to this package. I currently accept contributions from everyone but I expect that standards are maintained.
+To contribute,
+1. Fork the project
+1. Create a feature branch, branch away from `master`
+1. Write tests, using `Mocha and Chai` or any other testing frameworks, and code
+1. If you have multiple commits please combine them into a few logically organized commits by squashing them.
+1. Push the commit(s) to your fork
+1. Submit a merge request (MR) to the `master` branch
+1. The MR title should describe the change you want to make
+1. The MR description should give a motive for your change and the method you used to achieve it.
+ 1. Mention the issue(s) your merge request solves, using the `Solves #XXX` or
+ `Closes #XXX` syntax to auto-close the issue(s) once the merge request will
+ be merged.
+1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission
+ 1. If a discussion has been addressed, select the "Resolve discussion" button beneath it to mark it resolved.
+1. When writing commit messages please follow
+ [these guidelines](http://chris.beams.io/posts/git-commit).
-## CONTRIBUTING
-All contributions are welcome. Just create a push request, mention me and I will have a look at it.
\ No newline at end of file
+# License
+This project is licensed under the MIT license. Click **[here](./LICENSE.md)** to read the license in full
\ No newline at end of file
diff --git a/client/.babelrc b/client/.babelrc
new file mode 100755
index 0000000..586b56f
--- /dev/null
+++ b/client/.babelrc
@@ -0,0 +1,25 @@
+{
+ "presets": ["env", "react"],
+ "plugins": [
+ "transform-object-rest-spread",
+ "transform-es2015-destructuring",
+ [
+ "module-resolver",
+ {
+ "root": ["./src"],
+ "alias":{
+ "form": ["./src/components/form"],
+ "Modal": ["./src/components/Modal"]
+ }
+ }
+ ]
+ ],
+ "env": {
+ "production": {
+ "plugins": [
+ "transform-react-remove-prop-types",
+ "transform-react-inline-elements"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/.env.sample b/client/.env.sample
new file mode 100644
index 0000000..729127c
--- /dev/null
+++ b/client/.env.sample
@@ -0,0 +1,23 @@
+PORT=
+NODE_ENV=
+APP_CLOUD_NAME=
+APP_API_KEY=
+APP_API_SECRET=
+APP_CLOUDINARY_URL=
+
+
+AUTH_GOOGLE_CLIENT_ID=
+AUTH_GOOGLE_CLIENT_SECRET=
+AUTH_GOOGLE_CALLBACK=
+
+AUTH_FACEBOOK_CLIENT_ID=
+AUTH_FACEBOOK_CLIENT_SECRET=
+AUTH_FACEBOOK_CALLBACK=
+
+DISQUS_SHORT_NAME=
+ROOT_URL=
+
+TIMEZONE=
+SOCKET_URL=
+
+API_VERSION=
\ No newline at end of file
diff --git a/client/.eslintignore b/client/.eslintignore
new file mode 100755
index 0000000..c6ada23
--- /dev/null
+++ b/client/.eslintignore
@@ -0,0 +1,7 @@
+.idea/
+assets/**
+public/**
+coverage
+node_modules
+.env
+__tests__
\ No newline at end of file
diff --git a/client/.eslintrc b/client/.eslintrc
new file mode 100755
index 0000000..d750550
--- /dev/null
+++ b/client/.eslintrc
@@ -0,0 +1,50 @@
+{
+ "root": true,
+ "extends": [ "airbnb-base", "react" ],
+ "env": {
+ "node": true,
+ "es6": true,
+ "browser": true,
+ "jest": true
+ },
+ "settings": {
+ "import/resolver": {
+ "webpack": {
+ "config": "./config/webpack.dev.js"
+ },
+ "node": {
+ "extensions": [".js",".jsx", ".spec.js", ".spec.jsx"]
+ }
+ }
+ },
+ "rules": {
+ "indent": ["error", 2],
+ "prefer-reflect": 0,
+ "max-len": [1, 80, 2],
+ "one-var": 0,
+ "one-var-declaration-per-line": 0,
+ "new-cap": 0,
+ "consistent-return": 0,
+ "no-param-reassign": 0,
+ "comma-dangle": 0,
+ "curly": ["error", "multi-line"],
+ "import/no-unresolved": [2, { "commonjs": true }],
+ "no-shadow": ["error", { "allow": ["req", "res", "err"] }],
+ "valid-jsdoc": ["error", {
+ "requireReturn": true,
+ "requireReturnType": true,
+ "requireParamDescription": false,
+ "requireReturnDescription": true,
+ "matchDescription": ".+"
+ }],
+ "require-jsdoc": ["error", {
+ "require": {
+ "FunctionDeclaration": true,
+ "MethodDefinition": true,
+ "ClassDeclaration": true,
+ "ArrowFunctionExpression": true,
+ "FunctionExpression": true
+ }
+ }]
+ }
+ }
\ No newline at end of file
diff --git a/client/.hound.yml b/client/.hound.yml
new file mode 100755
index 0000000..9ad3511
--- /dev/null
+++ b/client/.hound.yml
@@ -0,0 +1,4 @@
+eslint:
+ enabled: true
+ config_file: .eslintrc
+ ignore_file: .eslintignore
\ No newline at end of file
diff --git a/client/__tests__/__mocks__/actions/book.js b/client/__tests__/__mocks__/actions/book.js
new file mode 100644
index 0000000..2c9caa7
--- /dev/null
+++ b/client/__tests__/__mocks__/actions/book.js
@@ -0,0 +1,41 @@
+export const bookData = {
+ title: 'A new title',
+ coverPhotoPath: '',
+ documentPath: ''
+};
+
+export const response = {
+ message: ['Book added successfully'],
+ book: {
+ title: 'A new book title',
+ id: 1
+ },
+ id: 1
+};
+
+export const searchResponse = {
+ books: {
+ ...response.book
+ }
+};
+
+export const getBookResponse = {
+ books: { ...response.book },
+ pagination: {
+ pageSize: 1,
+ totalCount: 10,
+ page: 0,
+ pageCount: 1,
+ limit: 10
+ },
+ message: ['There was an unexpected error']
+};
+
+export const responseFailure = {
+ message: ['The title field is required']
+};
+
+export const signinResponseFailure = {
+ message: ['Sorry, we can\'t find this account']
+};
+
diff --git a/client/__tests__/__mocks__/actions/borrowedBooks.js b/client/__tests__/__mocks__/actions/borrowedBooks.js
new file mode 100644
index 0000000..da88343
--- /dev/null
+++ b/client/__tests__/__mocks__/actions/borrowedBooks.js
@@ -0,0 +1,33 @@
+const book = {
+ userId: 1,
+ bookId: 1
+};
+
+export default {
+ book,
+ response: {
+ message: 'You have successfully borrowed this book',
+ id: 1,
+ ...book
+ },
+ error: {
+ message: ['There are no more copies left of this book to borrow']
+ },
+ getRequestData: {
+ userId: 1
+ },
+ getResponseData: {
+ books: {
+ ...book,
+ createdAt: '2017-01-01'
+ }
+ },
+ returnRequestData: {
+ userId: 1,
+ bookId: 1,
+ borrowedBookId: 1
+ },
+ returnResponseData: {
+ message: 'You have successfully returned this book'
+ }
+};
diff --git a/client/__tests__/__mocks__/actions/user.js b/client/__tests__/__mocks__/actions/user.js
new file mode 100644
index 0000000..e8bce03
--- /dev/null
+++ b/client/__tests__/__mocks__/actions/user.js
@@ -0,0 +1,17 @@
+
+export const response = {
+ message: ['Your account has been created successfully'],
+ user: {
+ group: 'user',
+ userId: 1,
+ }
+};
+
+export const signupResponseFailure = {
+ message: ['The name field is required']
+};
+
+export const signinResponseFailure = {
+ message: ['Sorry, we can\'t find this account']
+};
+
diff --git a/client/__tests__/__mocks__/file.js b/client/__tests__/__mocks__/file.js
new file mode 100644
index 0000000..ff8b4c5
--- /dev/null
+++ b/client/__tests__/__mocks__/file.js
@@ -0,0 +1 @@
+export default {};
diff --git a/client/__tests__/__mocks__/localStorage.js b/client/__tests__/__mocks__/localStorage.js
new file mode 100644
index 0000000..de031d8
--- /dev/null
+++ b/client/__tests__/__mocks__/localStorage.js
@@ -0,0 +1,46 @@
+
+/**
+ * @class localStorage
+ */
+class localStorage {
+ /**
+ * @static
+ * @param {any} key
+ * @param {any} value
+ * @returns {object} setItem
+ * @memberOf localStorage
+ */
+ static setItem(key, value) {
+ return Object.assign(localStorage, { [key]: value });
+ }
+
+ /**
+ * @static
+ * @param {any} key
+ * @returns {object} item
+ * @memberOf localStorage
+ */
+ static getItem(key) {
+ return localStorage[key];
+ }
+
+ /**
+ * @static
+ * @param {any} key
+ * @returns {undefined}
+ * @memberOf localStorage
+ */
+ static removeItem(key) {
+ delete localStorage[key];
+ }
+
+ /**
+ * @static
+ * @returns {undefined}
+ * @memberOf localStorage
+ */
+ static clear() {
+ localStorage = {};
+ }
+}
+export default localStorage;
diff --git a/client/__tests__/__mocks__/style.js b/client/__tests__/__mocks__/style.js
new file mode 100644
index 0000000..ff8b4c5
--- /dev/null
+++ b/client/__tests__/__mocks__/style.js
@@ -0,0 +1 @@
+export default {};
diff --git a/client/__tests__/__mocks__/worker.js b/client/__tests__/__mocks__/worker.js
new file mode 100644
index 0000000..727ede2
--- /dev/null
+++ b/client/__tests__/__mocks__/worker.js
@@ -0,0 +1 @@
+export default Object.create(null);
diff --git a/client/__tests__/actions/auth.spec.js b/client/__tests__/actions/auth.spec.js
new file mode 100644
index 0000000..3086761
--- /dev/null
+++ b/client/__tests__/actions/auth.spec.js
@@ -0,0 +1,312 @@
+import moxios from 'moxios';
+import thunk from 'redux-thunk';
+import configureMockStore from 'redux-mock-store';
+import expect from 'expect';
+
+import {
+ login,
+ logout,
+ sendResetPasswordMail,
+ resetPassword
+} from 'actions/auth';
+import {
+ userSignupRequestAction
+} from 'actions/signupActions';
+import {
+ response,
+ signupResponseFailure,
+ signinResponseFailure
+} from '../__mocks__/actions/user';
+
+/* eslint-disable max-nested-callbacks */
+
+const mockStore = configureMockStore([
+ thunk
+]);
+
+describe('# Auth', () => {
+ beforeEach(() => moxios.install());
+ afterEach(() => moxios.uninstall());
+
+ describe('# Signup', () => {
+ it(
+ 'creates SET_CURRENT_USER when signup action is successful',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: ['Your account has been created successfully'],
+ type: 'success'
+ }
+ },
+ { type: 'SET_CURRENT_USER', user: response.user }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(userSignupRequestAction({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+
+ it(
+ 'should not create a user when signup action fails',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: signupResponseFailure,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: ['The name field is required'],
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({});
+ return store.dispatch(userSignupRequestAction({})).then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('# Signin', () => {
+ it(
+ 'should create SET_CURRENT_USER when signin action is successful',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response,
+ });
+ });
+
+ const expectedActions = [
+ { type: 'SET_CURRENT_USER', user: response.user }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(login({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+
+ it(
+ 'should not log a user in when signin action fails',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: signinResponseFailure,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: ['Sorry, we can\'t find this account'],
+ type: 'error'
+ },
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(login({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('Logout', () => {
+ it(
+ 'should log a user out when the logout action is dispatched',
+ (done) => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response,
+ });
+ });
+
+ const expectedActions = [
+ { type: 'SET_CURRENT_USER', user: {} }
+ ];
+
+ const store = mockStore({ });
+ store.dispatch(logout());
+ expect(store.getActions()).toEqual(expectedActions);
+ done();
+ }
+ );
+ });
+
+ describe('Send Reset password mail', () => {
+ it(
+ 'should dispatch success flash message '+
+ 'action when password reset mail has been sent',
+ (done) => {
+ const message = [
+ 'A password reset link has been sent to test@hellobooks.com. ' +
+ 'It may take upto 5 mins for the mail to arrive.'
+ ];
+ moxios.stubRequest('users/reset-password', {
+ status: 200,
+ response: {
+ message
+ }
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(sendResetPasswordMail({}))
+ .then(() => {
+ const receivedOption = store.getActions();
+ expect(receivedOption).toEqual(expectedActions);
+ done();
+ })
+ .catch((error) => done(error));
+ }
+ );
+
+ it(
+ 'should dispatch error flash message ' +
+ 'action when password reset mail cannot be sent',
+ (done) => {
+ const message = [
+ 'There was an error processing your request'
+ ];
+ moxios.stubRequest('users/reset-password', {
+ status: 400,
+ response: {
+ message
+ }
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(sendResetPasswordMail({}))
+ .then(() => {
+ const receivedOption = store.getActions();
+ expect(receivedOption).toEqual(expectedActions);
+ done();
+ })
+ .catch((error) => done(error));
+ }
+ );
+ });
+ describe('Reset password', () => {
+ it(
+ 'should dispatch success flash message'+
+ ' action when password has been changed',
+ (done) => {
+ const message = [
+ 'Password successfully changed. Please login to your account.'
+ ];
+ moxios.stubRequest('users/reset-password/verify', {
+ status: 200,
+ response: {
+ message
+ }
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(resetPassword({}))
+ .then(() => {
+ const receivedOption = store.getActions();
+ expect(receivedOption).toEqual(expectedActions);
+ done();
+ })
+ .catch((error) => done(error));
+ }
+ );
+
+ it(
+ 'should dispatch error flash message ' +
+ 'action when password cannot be changed',
+ (done) => {
+ const message = [
+ 'There was an error processing your request'
+ ];
+ moxios.stubRequest('users/reset-password/verify', {
+ status: 400,
+ response: {
+ message
+ }
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(resetPassword({}))
+ .then(() => {
+ const receivedOption = store.getActions();
+ expect(receivedOption).toEqual(expectedActions);
+ done();
+ })
+ .catch((error) => done(error));
+ }
+ );
+ });
+});
diff --git a/client/__tests__/actions/books.spec.js b/client/__tests__/actions/books.spec.js
new file mode 100644
index 0000000..dde195e
--- /dev/null
+++ b/client/__tests__/actions/books.spec.js
@@ -0,0 +1,268 @@
+import moxios from 'moxios';
+import thunk from 'redux-thunk';
+import configureMockStore from 'redux-mock-store';
+import expect from 'expect';
+
+import {
+ addBook,
+ updateBook,
+ deleteBook,
+ searchBooks,
+ getBooks
+} from 'actions/books';
+import localStorage from '../__mocks__/localStorage';
+import {
+ bookData,
+ response,
+ searchResponse,
+ getBookResponse
+} from '../__mocks__/actions/book';
+
+/* eslint-disable max-nested-callbacks */
+
+const mockStore = configureMockStore([
+ thunk
+]);
+
+window.localStorage = localStorage;
+
+describe('# Books', () => {
+ beforeEach(() => moxios.install());
+ afterEach(() => moxios.uninstall());
+
+ describe('# Add a book', () => {
+ it('should create BOOK_ADDED when a book has been added', () => {
+ moxios.stubRequest('books', {
+ status: 200,
+ response
+ });
+
+ moxios
+ .stubRequest(
+ 'books/1?fields[]=coverPhotoPath&fields[]=documentPath',
+ {
+ status: 200,
+ response
+ }
+ );
+ const expectedActions = [
+ {
+ type: 'BOOK_ADDED',
+ book: response.book
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(addBook(bookData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('# Edit a book', () => {
+ it('should create BOOK_UPDATED action when a book has been edited',
+ () => {
+ const newResponse = {
+ ...response,
+ message: ['Book updated successfully']
+ };
+
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: newResponse,
+ });
+ });
+
+ moxios
+ .stubRequest('books/1?fields[]=coverPhotoPath&fields[]=documentPath',
+ {
+ status: 200,
+ response: newResponse
+ }
+ );
+
+ const expectedActions = [
+ {
+ type: 'BOOK_UPDATED',
+ book: response.book
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: ['Book updated successfully'],
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(updateBook(bookData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('# Delete a book', () => {
+ it('should create BOOK_DELETED action when a book has been deleted',
+ () => {
+ const bookId = 1;
+ const newResponse = {
+ ...response,
+ message: ['Book deleted successfully'],
+ bookId: 1
+ };
+
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: newResponse,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'BOOK_DELETED',
+ bookId
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: newResponse.message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(deleteBook({ id: bookId }))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+
+ it(
+ 'should not create BOOK_DELETED action when a book can\'t be deleted',
+ () => {
+ const newResponse = {
+ ...response,
+ message: ['Book not found'],
+ bookId: 1
+ };
+
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: newResponse,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: newResponse.message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(deleteBook({ id: 1 }))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('# Search a book', () => {
+ it(
+ 'should create BOOKS_SEARCHED action when a book has been searched',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: searchResponse,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'BOOKS_SEARCHED',
+ result: response.book,
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(searchBooks(bookData.title))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('# Get a book', () => {
+ it('should create SET_BOOKS action when a book has been gotten', () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: {
+ books: getBookResponse.books,
+ pagination: getBookResponse.pagination
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'SET_BOOKS',
+ books: getBookResponse.books,
+ pagination: getBookResponse.pagination,
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(getBooks(bookData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ it(
+ 'should not create SET_BOOKS action when a book can\'t be gotten',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 500,
+ response: getBookResponse,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: getBookResponse.message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(getBooks(bookData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+});
diff --git a/client/__tests__/actions/borrowedBooks.spec.js b/client/__tests__/actions/borrowedBooks.spec.js
new file mode 100644
index 0000000..3ed28bc
--- /dev/null
+++ b/client/__tests__/actions/borrowedBooks.spec.js
@@ -0,0 +1,210 @@
+import moxios from 'moxios';
+import thunk from 'redux-thunk';
+import configureMockStore from 'redux-mock-store';
+import expect from 'expect';
+
+import {
+ borrowBook,
+ getBorrowedBooks,
+ returnBorrowedBook
+} from 'actions/borrowedBooks';
+import borrowedBookMock from '../__mocks__/actions/borrowedBooks';
+
+/* eslint-disable max-nested-callbacks */
+
+const mockStore = configureMockStore([
+ thunk
+]);
+
+describe('# Borrowed Books', () => {
+ beforeEach(() => moxios.install());
+ afterEach(() => moxios.uninstall());
+
+ describe('Borrow a book', () => {
+ it(
+ 'should create BOOK_BORROWED and '+
+ 'ADD_FLASH_MESSAGE when a book has been borrowed',
+ () => {
+ moxios.stubRequest('users/1/books', {
+ status: 200,
+ response: borrowedBookMock.response
+ });
+
+ const expectedActions = [
+ {
+ type: 'BOOK_BORROWED',
+ book: borrowedBookMock.book,
+ isReturned: false
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: borrowedBookMock.response.message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(borrowBook(borrowedBookMock.book))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ it('should create BOOK_BORROWED when a book has been borrowed', () => {
+ moxios.stubRequest('users/1/books', {
+ status: 400,
+ response: borrowedBookMock.error
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: borrowedBookMock.error.message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(borrowBook(borrowedBookMock.book))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('Get Borrowed book', () => {
+ it(
+ 'should create GET_BORROWED_BOOKS when request is successful',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: borrowedBookMock.getResponseData,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'GET_BORROWED_BOOKS',
+ book: borrowedBookMock.getResponseData.books
+ }
+ ];
+
+ const store = mockStore({ });
+ return store
+ .dispatch(getBorrowedBooks(borrowedBookMock.getRequestData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+
+ it(
+ 'should create ADD_FLASH_MESSAGE when request is not successful',
+ () => {
+ const message = ['There was an error processing your request'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: {
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store
+ .dispatch(getBorrowedBooks(borrowedBookMock.getRequestData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('Returned Borrowed book', () => {
+ it(
+ 'should create BOOK_RETURNED when request is successful',
+ () => {
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: borrowedBookMock.returnResponseData,
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'BOOK_RETURNED',
+ book: borrowedBookMock.returnRequestData,
+ isReturned: true
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: borrowedBookMock.returnResponseData.message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store
+ .dispatch(returnBorrowedBook(borrowedBookMock.returnRequestData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+
+ it(
+ 'should create error ADD_FLASH_MESSAGE when request is not successful',
+ () => {
+ const message = ['There was an error completing your request'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: {
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store
+ .dispatch(returnBorrowedBook(borrowedBookMock.returnRequestData))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+});
diff --git a/client/__tests__/actions/categories.spec.js b/client/__tests__/actions/categories.spec.js
new file mode 100644
index 0000000..447cfba
--- /dev/null
+++ b/client/__tests__/actions/categories.spec.js
@@ -0,0 +1,192 @@
+import moxios from 'moxios';
+import thunk from 'redux-thunk';
+import configureMockStore from 'redux-mock-store';
+import expect from 'expect';
+
+import {
+ addBookCategory,
+ getBookCategories,
+ editBookCategory
+} from 'actions/categories';
+
+/* eslint-disable max-nested-callbacks */
+
+const mockStore = configureMockStore([
+ thunk
+]);
+
+const category = {
+ id: 1,
+ name: 'Category1'
+};
+
+describe('# Categories', () => {
+ beforeEach(() => moxios.install());
+ afterEach(() => moxios.uninstall());
+
+ describe('# Add a book category', () => {
+ it('should create CATEGORY_ADDED when a category has been added',
+ () => {
+ const message = ['Category added successfully'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: {
+ category,
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'CATEGORY_ADDED',
+ category
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'success'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(addBookCategory({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+
+ it(
+ 'should create ADD_FLASH_MESSAGE when a category cannot be added',
+ () => {
+ const message = ['There was an error processing your request'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: {
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'error'
+ }
+ }
+ ];
+
+ const store = mockStore({ });
+ return store.dispatch(addBookCategory({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+
+ describe('# Get book category', () => {
+ it(
+ 'should create CATEGORY_FETCHED when a category has been fetched',
+ () => {
+ const message = ['Category added successfully'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: {
+ categories: category,
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'CATEGORY_FETCHED',
+ categories: category
+ }
+ ];
+ const store = mockStore({ });
+ return store.dispatch(getBookCategories({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+
+ it('should create ADD_FLASH_MESSAGE when a category can\'t ', () => {
+ const message = ['Category added successfully'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 400,
+ response: {
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ type: 'error',
+ text: message
+ }
+ }
+ ];
+ const store = mockStore({ });
+ return store.dispatch(getBookCategories({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ });
+ });
+
+ describe('# Edit book category', () => {
+ it(
+ 'should create CATEGORY_EDITED when a category has been edited',
+ () => {
+ const message = ['Category edited successfully'];
+ moxios.wait(() => {
+ const request = moxios.requests.mostRecent();
+ request.respondWith({
+ status: 200,
+ response: {
+ category,
+ message
+ },
+ });
+ });
+
+ const expectedActions = [
+ {
+ type: 'CATEGORY_EDITED',
+ category
+ },
+ {
+ type: 'ADD_FLASH_MESSAGE',
+ message: {
+ text: message,
+ type: 'success'
+ }
+ }
+ ];
+ const store = mockStore({ });
+ return store.dispatch(editBookCategory({}))
+ .then(() => {
+ expect(store.getActions()).toEqual(expectedActions);
+ });
+ }
+ );
+ });
+});
diff --git a/client/__tests__/assets/js/modal.spec.js b/client/__tests__/assets/js/modal.spec.js
new file mode 100644
index 0000000..da380cb
--- /dev/null
+++ b/client/__tests__/assets/js/modal.spec.js
@@ -0,0 +1,9 @@
+import React from 'react';
+import { onClickOpenBookCover } from 'assets/js/modal';
+
+describe('# onClickOpenBookCover', () => {
+ it('should call onClickOpenBookCover', () => {
+ expect(onClickOpenBookCover(global.event, '#target'))
+ .toBeUndefined();
+ });
+});
diff --git a/client/__tests__/assets/js/removeBookBg.spec.js b/client/__tests__/assets/js/removeBookBg.spec.js
new file mode 100644
index 0000000..70c9db2
--- /dev/null
+++ b/client/__tests__/assets/js/removeBookBg.spec.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import removeBookBg from 'assets/js/removeBookBg';
+
+describe('# removeBookBg', () => {
+ it('should call removeBookBg', () => {
+ expect(removeBookBg()).toBeUndefined();
+ });
+});
diff --git a/client/__tests__/assets/js/socket/index.spec.js b/client/__tests__/assets/js/socket/index.spec.js
new file mode 100644
index 0000000..0415608
--- /dev/null
+++ b/client/__tests__/assets/js/socket/index.spec.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import {
+ ioGetNotifications,
+ ioJoin,
+ ioNewNotifications } from 'assets/js/socket/';
+
+describe('# Socket', () => {
+ it('should call ioGetNotifications', () => {
+ expect(ioGetNotifications()).toBeUndefined();
+ });
+
+ it('should call ioJoin', () => {
+ expect(ioJoin()).toBeUndefined();
+ });
+
+ it('should call ioNewNotifications', () => {
+ expect(ioNewNotifications()).toBeUndefined();
+ });
+});
diff --git a/client/__tests__/components/App.spec.js b/client/__tests__/components/App.spec.js
new file mode 100644
index 0000000..1a44660
--- /dev/null
+++ b/client/__tests__/components/App.spec.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import App from 'components/App';
+
+jest.mock('react-router');
+
+const props = {
+ children: