├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── Procfile ├── README.md ├── config └── config.js ├── index.js ├── migrations ├── 20180627035134-create-post.js ├── 20180627040747-create-terms.js ├── 20180627224006-create-user.js └── 20180718230542-create-post-terms.js ├── models ├── index.js ├── post.js ├── term.js └── user.js ├── package.json ├── seeders ├── posts.js ├── terms.js └── users.js ├── server ├── api │ ├── api.js │ ├── post │ │ ├── controller.js │ │ └── routes.js │ └── user │ │ ├── controller.js │ │ └── routes.js ├── auth │ ├── controller.js │ ├── passport.js │ └── routes.js ├── config │ ├── config.js │ └── middlewares.js ├── server.js └── utils │ └── helpers.js ├── test ├── auth │ └── auth.test.js ├── post │ └── post.test.js ├── seedpress-api.postman_collection.json ├── user │ └── user.test.js └── utils │ └── helpers.test.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true, 6 | }, 7 | "extends": "eslint:recommended", 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "ecmaVersion": 2017 11 | }, 12 | "rules": { 13 | "comma-dangle": [2, "never"], 14 | "no-console": 1, 15 | "no-constant-condition": 2, 16 | "no-control-regex": 2, 17 | "no-debugger": 1, 18 | "no-dupe-keys": 2, 19 | "no-duplicate-case": 2, 20 | "no-empty-character-class": 2, 21 | "no-empty": 2, 22 | "no-extra-boolean-cast": 2, 23 | "no-extra-semi": 2, 24 | "no-func-assign": 2, 25 | "no-inner-declarations": 2, 26 | "no-invalid-regexp": 2, 27 | "no-irregular-whitespace": 2, 28 | "no-negated-in-lhs": 2, 29 | "no-unreachable": 2, 30 | "use-isnan": 2, 31 | "valid-typeof": 2, 32 | 33 | "accessor-pairs": 2, 34 | "block-scoped-var": 2, 35 | "curly": 2, 36 | "default-case": 2, 37 | "dot-location": [2, "property"], 38 | "dot-notation": 2, 39 | "eqeqeq": [2, "smart"], 40 | "no-alert": 1, 41 | "no-caller": 2, 42 | "no-case-declarations": 2, 43 | "no-div-regex": 1, 44 | "no-else-return": 2, 45 | "no-eval": 2, 46 | "no-extend-native": 2, 47 | "no-extra-bind": 2, 48 | "no-fallthrough": 2, 49 | "no-floating-decimal": 2, 50 | "no-implied-eval": 2, 51 | "no-iterator": 2, 52 | "no-labels": 2, 53 | "no-lone-blocks": 2, 54 | "no-loop-func": 2, 55 | "no-multi-spaces": 2, 56 | "no-multi-str": 2, 57 | "no-native-reassign": 2, 58 | "no-new-func": 2, 59 | "no-new-wrappers": 2, 60 | "no-new": 2, 61 | "no-octal-escape": 2, 62 | "no-octal": 2, 63 | "no-param-reassign": 2, 64 | "no-proto": 2, 65 | "no-redeclare": 2, 66 | "no-return-assign": 2, 67 | "no-self-compare": 2, 68 | "no-throw-literal": 2, 69 | "no-unused-expressions": [2, { 70 | "allowShortCircuit": true 71 | }], 72 | "no-useless-call": 2, 73 | "no-useless-concat": 2, 74 | "no-void": 2, 75 | "no-warning-comments": 1, 76 | "no-with": 2, 77 | "radix": [2, "as-needed"], 78 | "vars-on-top": 2, 79 | "wrap-iife": [2, "inside"], 80 | "yoda": 2, 81 | 82 | "strict": [2, "function"], 83 | 84 | "no-catch-shadow": 2, 85 | "no-delete-var": 2, 86 | "no-shadow-restricted-names": 2, 87 | "no-shadow": 2, 88 | "no-undef-init": 2, 89 | "no-unused-vars": 2, 90 | "no-use-before-define": 2, 91 | 92 | "global-require": 2, 93 | "handle-callback-err": 1, 94 | "no-new-require": 2, 95 | 96 | "brace-style": [2, "1tbs", { 97 | "allowSingleLine": false 98 | }], 99 | "comma-style": [2, "last"], 100 | "consistent-this": [2, "self"], 101 | "consistent-return": 0, 102 | "eol-last": 2, 103 | "indent": [0, 2], 104 | "new-cap": 0, 105 | "new-parens": 2, 106 | "no-array-constructor": 2, 107 | "no-bitwise": 2, 108 | "no-continue": 2, 109 | "no-lonely-if": 2, 110 | "no-mixed-spaces-and-tabs": 2, 111 | "no-multiple-empty-lines": [2, { 112 | "max": 2, 113 | "maxEOF": 1 114 | }], 115 | "no-nested-ternary": 2, 116 | "no-new-object": 2, 117 | "no-spaced-func": 2, 118 | "no-trailing-spaces": [2, { 119 | "skipBlankLines": true 120 | }], 121 | "no-unneeded-ternary": 2, 122 | "one-var": 0, 123 | "operator-linebreak": [2, "none"], 124 | "quotes": [2, "single", "avoid-escape"], 125 | "semi": [2, "always"], 126 | 127 | "arrow-parens": [2, "always"], 128 | "arrow-spacing": 2, 129 | "constructor-super": 2, 130 | "no-class-assign": 2, 131 | "no-const-assign": 2, 132 | "no-dupe-class-members": 2, 133 | "no-this-before-super": 2, 134 | "no-var": 2, 135 | "prefer-arrow-callback": 1, 136 | "prefer-spread": 1, 137 | "prefer-template": 1, 138 | "require-yield": 2 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | logfile 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | *.pid.lock 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | codecov.yml 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # Typescript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: 2 | node_js 3 | node_js: 4 | - "8.11" 5 | services: 6 | - postgresql 7 | env: 8 | - NODE_ENV=test 9 | cache: 10 | directories: 11 | - node_modules 12 | before_install: 13 | # Update Node.js modules 14 | - "test ! -d node_modules || npm prune" 15 | - "test ! -d node_modules || npm rebuild" 16 | install: 17 | - npm install 18 | before_script: 19 | - psql -c 'CREATE DATABASE seedpress_test;' -U postgres 20 | - psql -c "CREATE USER root WITH PASSWORD 'password';" -U postgres 21 | - sequelize db:migrate 22 | - sequelize db:seed:all 23 | script: 24 | - "npm run coverage" 25 | - ./node_modules/.bin/codecov 26 | - "npm run test" 27 | - "npm run lint" 28 | after_script: "npm -i coveralls@3.0.2 && cat ./coverage/lcov.info | coveralls" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ezy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node web.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 🌱 Seedpress Express API 2 | 3 | [![Build Status](https://travis-ci.com/ezy/seedpress-cms.svg?branch=master)](https://travis-ci.com/ezy/seedpress-cms) 4 | [![codecov.io](http://codecov.io/github/ezy/seedpress-cms/coverage.svg?branch=master)](http://codecov.io/github/ezy/seedpress-cms?branch=master) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 6 | [![Twitter Follow](https://img.shields.io/twitter/follow/__ezy__.svg?style=social&label=Follow)](https://twitter.com/__ezy__) 7 | 8 | Seedpress is a headless Node JS API server built with Express, for PostgreSQL using Sequelize ORM. It generally follows the logic and conventions of the Wordpress schema. Seedpress authenticates users using JSON web tokens managed by Passport. It's production ready, and should work as a great starter for any content based Progressive Web Application. 9 | 10 | * [Node.js](https://nodejs.org/en/) 11 | * [PostgreSQL](https://www.postgresql.org/) 12 | * [Sequelize](http://docs.sequelizejs.com/en/v3/) 13 | * [Sequelize-CLI](https://github.com/sequelize/cli) 14 | * [JSON Web Token](https://jwt.io/) 15 | 16 | **Hosting** 17 | 18 | * [Heroku](https://www.heroku.com/) 19 | 20 | **Testing** 21 | 22 | * [Mocha](https://mochajs.org/) 23 | * [Chai](http://chaijs.com/) 24 | * [Supertest](https://github.com/visionmedia/supertest) 25 | 26 | 27 | ### Features 28 | 29 | * [X] Authentication with JSON Web Token 30 | * [X] Email, Password validations 31 | * [X] User login and registration 32 | * [X] Full API and Unit test coverage 33 | * [X] Page and Term models with belongsToMany two way relationship 34 | * [X] Easily deployable to Heroku (Procfile) 35 | 36 | ### Schema 37 | 38 | * Post 39 | * id 40 | * postTitle 41 | * postSlug 42 | * postType 43 | * postDate 44 | * postContent 45 | * postAuthor 46 | * postImage 47 | * postMedia 48 | * postStatus 49 | * postExpiry 50 | * postFrequency 51 | * postTerms 52 | * createdAt 53 | * updatedAt 54 | 55 | * Terms 56 | * id 57 | * termType 58 | * termName 59 | * createdAt 60 | * updatedAt 61 | 62 | * PostTerms 63 | * termId 64 | * postId 65 | * createdAt 66 | * updatedAt 67 | 68 | ### To run locally 69 | 70 | Make sure to install and run PostgreSQL first. There are a list of package.json 71 | scripts that include core sequelize migrations and seeds. 72 | 73 | ``` 74 | brew update 75 | brew install postgres 76 | ``` 77 | Then you'll need to adjust the config file `config/config.js` to suit your setup. 78 | 79 | Then run `yarn reset` and your db will create from scratch and seed data under 80 | the db name `seedpress_dev`. 81 | 82 | Running `yarn dev` will start your dev server where needed. 83 | 84 | ### Testing 85 | 86 | You'll need to setup the test server using `yarn resettest`. To trigger the mocha/chai tests enter: 87 | ``` 88 | yarn test 89 | ``` 90 | 91 | #### Postman 92 | 93 | There is a postman collection file for API testing and development in 94 | `test/seedpress-api.postman_collection.json`. It contains scripts and 95 | basic CRUD for: 96 | 97 | * /auth 98 | * [POST] Register (/register) 99 | * [POST] Login (/login) 100 | 101 | * /posts 102 | * [POST] Single post (/ - with auth) 103 | * [PATCH] Single post (/:slug - with auth) 104 | * [DEL] Single post (/:slug - with auth) 105 | * [GET] All posts (/) 106 | * [GET] Single post (/:slug) 107 | 108 | ### To deploy on Heroku 109 | ``` 110 | heroku login 111 | heroku create 112 | git push heroku master 113 | ``` 114 | 115 | #### Sequelize ORM 116 | 117 | Seedpress uses Sequelise ORM to interact with Postgres. Install globally 118 | on your dev machine using `yarn global sequelize-cli` then run commands with 119 | `sequelize` or alternately run commands locally in your dev folder with the 120 | built in dev package `./node_modules/.bin/sequelize init`. 121 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'development': { 3 | 'username': 'postgres', 4 | 'password': null, 5 | 'database': 'seedpress_dev', 6 | 'host': '127.0.0.1', 7 | 'dialect': 'postgres', 8 | 'operatorsAliases': false 9 | }, 10 | 'test': { 11 | 'username': 'postgres', 12 | 'password': null, 13 | 'database': 'seedpress_test', 14 | 'host': '127.0.0.1', 15 | 'dialect': 'postgres', 16 | 'operatorsAliases': false 17 | }, 18 | 'production': { 19 | 'username': 'postgres', 20 | 'password': null, 21 | 'database': 'seedpress_production', 22 | 'host': '127.0.0.1', 23 | 'dialect': 'postgres', 24 | 'operatorsAliases': false 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const app = require('./server/server'); 2 | const config = require('./server/config/config'); 3 | 4 | // Start listening 5 | app.listen(config.port, () => { 6 | console.log('Server started http://localhost:%s', config.port); 7 | }); 8 | -------------------------------------------------------------------------------- /migrations/20180627035134-create-post.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable('Posts', { 5 | id: { 6 | allowNull: false, 7 | primaryKey: true, 8 | type: Sequelize.INTEGER, 9 | autoIncrement: true 10 | }, 11 | postTitle: { 12 | type: Sequelize.STRING 13 | }, 14 | postSlug: { 15 | type: Sequelize.STRING, 16 | unique: true 17 | }, 18 | postType: { 19 | type: Sequelize.STRING 20 | }, 21 | postDate: { 22 | type: Sequelize.DATE 23 | }, 24 | postContent: { 25 | type: Sequelize.TEXT 26 | }, 27 | postAuthor: { 28 | type: Sequelize.STRING 29 | }, 30 | postImage: { 31 | type: Sequelize.STRING 32 | }, 33 | postMedia: { 34 | type: Sequelize.STRING 35 | }, 36 | postStatus: { 37 | type: Sequelize.STRING 38 | }, 39 | postExpiry: { 40 | type: Sequelize.DATE 41 | }, 42 | postFrequency: { 43 | type: Sequelize.STRING 44 | }, 45 | createdAt: { 46 | type: Sequelize.DATE 47 | }, 48 | updatedAt: { 49 | type: Sequelize.DATE 50 | } 51 | }); 52 | }, 53 | down: (queryInterface) => { 54 | return queryInterface.dropTable('Posts'); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /migrations/20180627040747-create-terms.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Terms', { 4 | id: { 5 | allowNull: false, 6 | primaryKey: true, 7 | type: Sequelize.INTEGER, 8 | autoIncrement: true 9 | }, 10 | termType: { 11 | type: Sequelize.STRING 12 | }, 13 | termName: { 14 | type: Sequelize.STRING 15 | }, 16 | termSlug: { 17 | type: Sequelize.STRING 18 | }, 19 | createdAt: { 20 | type: Sequelize.DATE 21 | }, 22 | updatedAt: { 23 | type: Sequelize.DATE 24 | } 25 | }); 26 | }, 27 | down: (queryInterface) => { 28 | return queryInterface.dropTable('Terms'); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /migrations/20180627224006-create-user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('Users', { 4 | id: { 5 | allowNull: false, 6 | primaryKey: true, 7 | type: Sequelize.UUID 8 | }, 9 | userName: { 10 | type: Sequelize.STRING 11 | }, 12 | userEmail: { 13 | type: Sequelize.STRING 14 | }, 15 | userPass: { 16 | type: Sequelize.STRING 17 | }, 18 | createdAt: { 19 | type: Sequelize.DATE 20 | }, 21 | updatedAt: { 22 | type: Sequelize.DATE 23 | } 24 | }); 25 | }, 26 | down: (queryInterface) => { 27 | return queryInterface.dropTable('Users'); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /migrations/20180718230542-create-post-terms.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: (queryInterface, Sequelize) => { 3 | return queryInterface.createTable('PostTerms', { 4 | postId: { 5 | type: Sequelize.INTEGER 6 | }, 7 | termId: { 8 | type: Sequelize.INTEGER 9 | }, 10 | createdAt: { 11 | type: Sequelize.DATE 12 | }, 13 | updatedAt: { 14 | type: Sequelize.DATE 15 | } 16 | }); 17 | }, 18 | down: (queryInterface) => { 19 | return queryInterface.dropTable('PostTerms'); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let path = require('path'); 3 | let Sequelize = require('sequelize'); 4 | let basename = path.basename(__filename); 5 | let env = process.env.NODE_ENV || 'development'; 6 | let config = require('../config/config.js')[env]; 7 | let db = {}; 8 | let sequelize; 9 | 10 | if (config.use_env_variable) { 11 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 12 | } else { 13 | sequelize = new Sequelize(config.database, config.username, config.password, config); 14 | } 15 | 16 | fs 17 | .readdirSync(__dirname) 18 | .filter((file) => { 19 | return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'); 20 | }) 21 | .forEach((file) => { 22 | let model = sequelize.import(path.join(__dirname, file)); 23 | db[model.name] = model; 24 | }); 25 | 26 | Object.keys(db).forEach((modelName) => { 27 | if (db[modelName].associate) { 28 | db[modelName].associate(db); 29 | } 30 | }); 31 | 32 | db.sequelize = sequelize; 33 | db.Sequelize = Sequelize; 34 | 35 | module.exports = db; 36 | -------------------------------------------------------------------------------- /models/post.js: -------------------------------------------------------------------------------- 1 | const date = new Date(); 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | const Post = sequelize.define('Post', { 5 | id: { 6 | type: DataTypes.INTEGER, 7 | primaryKey: true, 8 | autoIncrement: true, 9 | allowNull: false 10 | }, 11 | postTitle: { 12 | type: DataTypes.STRING, 13 | allowNull: false 14 | }, 15 | postSlug: { 16 | type: DataTypes.STRING, 17 | allowNull: false 18 | }, 19 | postType: { 20 | type: DataTypes.STRING, 21 | defaultValue: 'post', 22 | allowNull: false 23 | }, 24 | postDate: { 25 | type: DataTypes.DATE, 26 | defaultValue: date, 27 | allowNull: false 28 | }, 29 | postContent: DataTypes.TEXT, 30 | postAuthor: DataTypes.STRING, 31 | postImage: DataTypes.STRING, 32 | postMedia: DataTypes.STRING, 33 | postStatus: DataTypes.STRING, 34 | postExpiry: DataTypes.DATE, 35 | postFrequency: DataTypes.STRING 36 | }, {}); 37 | Post.associate = (models) => { 38 | Post.belongsToMany(models.Term, { 39 | through: 'PostTerms', 40 | as: 'postTerms', 41 | foreignKey: 'postId' 42 | }); 43 | }; 44 | return Post; 45 | }; 46 | -------------------------------------------------------------------------------- /models/term.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Terms are used for categorising posts. They belong to many posts and can 3 | * have many labels. Need to create a post with tags? Then you might create a post 4 | * with a 'tag' term: 5 | * 6 | * postTerms: [{ 7 | * termType: tag, 8 | * termName: business 9 | * },{ 10 | * termType: tag, 11 | * termName: pleasure 12 | * }] 13 | */ 14 | 15 | module.exports = (sequelize, DataTypes) => { 16 | const Term = sequelize.define('Term', { 17 | id: { 18 | type: DataTypes.INTEGER, 19 | primaryKey: true, 20 | autoIncrement: true, 21 | allowNull: false 22 | }, 23 | termType: { 24 | type: DataTypes.STRING 25 | }, 26 | termName: { 27 | type: DataTypes.STRING 28 | }, 29 | termSlug: { 30 | type: DataTypes.STRING, 31 | unique: true 32 | } 33 | }, {}); 34 | Term.associate = (models) => { 35 | Term.belongsToMany(models.Post, { 36 | through: 'PostTerms', 37 | as: 'postTerms', 38 | foreignKey: 'termId' 39 | }); 40 | }; 41 | return Term; 42 | }; 43 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | let User = sequelize.define('User', { 3 | id: { 4 | type: DataTypes.UUID, 5 | defaultValue: DataTypes.UUIDV4, 6 | primaryKey: true, 7 | allowNull: false, 8 | unique: true 9 | }, 10 | userName: DataTypes.STRING, 11 | userEmail: DataTypes.STRING, 12 | userPass: DataTypes.STRING 13 | }, {}); 14 | return User; 15 | }; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seedpress-api", 3 | "version": "0.1.0", 4 | "description": "A headless CMS built in Express for PostgresQL using Sequelize. Generally follows the Wordpress page, post and media schema.", 5 | "engines": { 6 | "node": "^8.*", 7 | "npm": "^5.6" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "lint": "eslint server ; exit 0", 12 | "dropdev": "psql -c 'DROP DATABASE IF EXISTS seedpress_dev' && psql -c 'CREATE DATABASE seedpress_dev'", 13 | "reset": "yarn dropdev && sequelize db:migrate && sequelize db:seed:all && yarn dev", 14 | "dev": "NODE_ENV=development nodemon index.js", 15 | "stest": "NODE_ENV=test nodemon index.js", 16 | "start": "NODE_ENV=production node index.js", 17 | "startdb": "pg_ctl -D /usr/local/var/postgres start", 18 | "stopdb": "pg_ctl -D /usr/local/var/postgres stop", 19 | "droptest": "psql -c 'DROP DATABASE IF EXISTS seedpress_test' && psql -c 'CREATE DATABASE seedpress_test'", 20 | "resettest": "yarn droptest && ./node_modules/.bin/sequelize db:migrate --env test && ./node_modules/.bin/sequelize db:seed:all --env test", 21 | "test": "export NODE_ENV=test && sequelize db:migrate:undo:all && sequelize db:migrate && sequelize db:seed:all &&./node_modules/.bin/mocha -R spec 'test/**/**.test.js'", 22 | "coverage": "istanbul cover _mocha -- -R spec 'test/**/**/**/**test.js'" 23 | }, 24 | "author": "Ezra Keddell", 25 | "license": "MIT", 26 | "dependencies": { 27 | "bcrypt": "^2.0.1", 28 | "body-parser": "^1.15.2", 29 | "change-case": "^3.0.2", 30 | "compression": "^1.6.2", 31 | "cors": "^2.8.1", 32 | "express": "^4.16.3", 33 | "global": "^4.3.2", 34 | "helmet": "^3.1.0", 35 | "jsonwebtoken": "^8.3.0", 36 | "passport": "^0.4.0", 37 | "passport-jwt": "^4.0.0", 38 | "passport-local": "^1.0.0", 39 | "pg": "^7.4.3", 40 | "pg-hstore": "^2.3.2", 41 | "sequelize": "^4.38.0", 42 | "sequelize-cli": "^4.0.0" 43 | }, 44 | "devDependencies": { 45 | "chai": "^3.5.0", 46 | "codecov": "^3.0.4", 47 | "eslint": "^3.11.1", 48 | "eslint-plugin-import": "^2.2.0", 49 | "faker": "^4.1.0", 50 | "istanbul": "^0.4.5", 51 | "mocha": "^3.2.0", 52 | "mocha-lcov-reporter": "^1.3.0", 53 | "morgan": "^1.7.0", 54 | "nodemon": "^1.17.4", 55 | "supertest": "^2.0.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /seeders/posts.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const changeCase = require('change-case'); 3 | 4 | let postsList = []; 5 | 6 | for (let i = 0; i < 2; i++) { 7 | let title = faker.lorem.sentence(5); 8 | const postObj = { 9 | id: faker.random.number(100000), 10 | postTitle: title, 11 | postSlug: `${changeCase.paramCase(title)}-${Date.now()}`, 12 | postType: faker.random.arrayElement(['post','page']), 13 | postDate: new Date(), 14 | postContent: faker.lorem.sentences(3,3), 15 | postAuthor: faker.name.findName(), 16 | postImage: faker.image.imageUrl(), 17 | postMedia: faker.image.imageUrl(), 18 | postStatus: faker.random.arrayElement(['published','draft']), 19 | postExpiry: faker.date.future(), 20 | postFrequency: faker.random.arrayElement([null,'day','week','fortnight','month']), 21 | createdAt: new Date(), 22 | updatedAt: new Date() 23 | }; 24 | postsList.push(postObj); 25 | } 26 | 27 | module.exports = { 28 | up: async (queryInterface) => { 29 | await queryInterface.bulkInsert('Posts', postsList, {}); 30 | }, 31 | 32 | down: async (queryInterface) => { 33 | await queryInterface.bulkDelete('Posts', null, {}); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /seeders/terms.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const changeCase = require('change-case'); 3 | 4 | let termList = []; 5 | 6 | for (let i = 0; i < 6; i++) { 7 | const name = faker.hacker.noun(); 8 | const type = faker.hacker.noun(); 9 | const termObj = { 10 | id: faker.random.number(100000), 11 | termType: type, 12 | termName: name, 13 | termSlug: `${changeCase.paramCase(type)}-${changeCase.paramCase(name)}`, 14 | createdAt: new Date(), 15 | updatedAt: new Date() 16 | }; 17 | termList.push(termObj); 18 | } 19 | 20 | module.exports = { 21 | up: async (queryInterface) => { 22 | await queryInterface.bulkInsert('Terms', termList, {}); 23 | }, 24 | 25 | down: async (queryInterface) => { 26 | await queryInterface.bulkDelete('Terms', null, {}); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /seeders/users.js: -------------------------------------------------------------------------------- 1 | const faker = require('faker'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | let usersList = []; 5 | 6 | for (let i = 0; i < 6; i++) { 7 | const userObj = { 8 | id: faker.random.uuid(), 9 | userName: faker.name.firstName(), 10 | userEmail: faker.internet.email(), 11 | userPass: bcrypt.hashSync('passwrod', 10), 12 | createdAt: new Date(), 13 | updatedAt: new Date() 14 | }; 15 | usersList.push(userObj); 16 | } 17 | 18 | module.exports = { 19 | up: (queryInterface) => { 20 | return queryInterface.bulkInsert('Users', usersList.concat([{ 21 | id: faker.random.uuid(), 22 | userName: 'John User', 23 | userEmail: 'user@email.com', 24 | userPass: bcrypt.hashSync('passwrod', 10), 25 | createdAt: new Date(), 26 | updatedAt: new Date() 27 | }]), {}); 28 | }, 29 | 30 | down: (queryInterface) => { 31 | return queryInterface.bulkDelete('Users', null, {}); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /server/api/api.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | 3 | // api router will mount other routers for all our resources 4 | router.use('/users', require('./user/routes')); 5 | router.use('/posts', require('./post/routes')); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /server/api/post/controller.js: -------------------------------------------------------------------------------- 1 | const Post = require('../../../models').Post; 2 | const Term = require('../../../models').Term; 3 | const changeCase = require('change-case'); 4 | 5 | // Create new post 6 | function createPost(req, res) { 7 | const postTitle = req.body.postTitle ? req.body.postTitle.trim() : null; 8 | let postSlug = `${changeCase.paramCase(postTitle)}-${Date.now()}`; 9 | const postType = req.body.postType ? req.body.postType.trim() : 'post'; 10 | const postDate = req.body.postDate ? req.body.postDate : new Date(); 11 | const postContent = req.body.postContent ? req.body.postContent.trim() : null; 12 | const postAuthor = req.body.postAuthor ? req.body.postAuthor.trim() : null; 13 | const postImage = req.body.postImage ? req.body.postImage.trim() : null; 14 | const postMedia = req.body.postMedia ? req.body.postMedia.trim() : null; 15 | const postStatus = req.body.postStatus ? req.body.postStatus.trim() : 'draft'; 16 | const postExpiry = req.body.postExpiry ? req.body.postExpiry.trim() : null; 17 | const postFrequency = req.body.postFrequency ? req.body.postFrequency.trim() : null; 18 | 19 | const postTerms = req.body.postTerms ? req.body.postTerms : []; 20 | 21 | 22 | if (!postTitle) { 23 | return res.status(422).send({ 24 | error: 'A postTitle is required.' 25 | }); 26 | } 27 | 28 | postTerms.forEach((term) => { 29 | let termType = term.termType; 30 | let termName = term.termName; 31 | if (!termName || !termType) { 32 | return res.status(422).send({ 33 | error: 'All terms require a termType and termName.' 34 | }); 35 | } 36 | term.termSlug = `${changeCase.paramCase(termType)}-${changeCase.paramCase(termName)}`; 37 | }); 38 | 39 | let newPost = { 40 | postTitle, 41 | postSlug, 42 | postType, 43 | postDate, 44 | postContent, 45 | postAuthor, 46 | postImage, 47 | postMedia, 48 | postStatus, 49 | postExpiry, 50 | postFrequency, 51 | postTerms 52 | }; 53 | 54 | Post.create(newPost, { 55 | include: [{ 56 | model: Term, 57 | as: 'postTerms' 58 | }] 59 | }) 60 | .then((post) => { 61 | return res.json({post: post}); 62 | }) 63 | .catch((err) => res.status(400).send({ 64 | error: err.errors 65 | })); 66 | } 67 | 68 | // Get all posts 69 | function getAllPosts(req, res) { 70 | Post.findAll({ include: [{ 71 | model: Term, 72 | as: 'postTerms', 73 | required: false, 74 | attributes: ['id','termType','termName','termSlug'], 75 | through: { attributes: [] } 76 | }] 77 | }) 78 | .then((posts) => { 79 | return res.json({posts}); 80 | }) 81 | .catch((err) => res.status(400).send({ 82 | error: err.message 83 | })); 84 | } 85 | 86 | // Get one post 87 | function getPost(req, res) { 88 | const postSlug = req.params.postSlug; 89 | Post.findOne({where: { postSlug }, include: [{ 90 | model: Term, 91 | as: 'postTerms', 92 | required: false, 93 | attributes: ['id','termType','termName','termSlug'], 94 | through: { attributes: [] } 95 | }] 96 | }) 97 | .then((post) => { 98 | if (!post) { 99 | return res.status(400).send({ 100 | error: 'No post found' 101 | }); 102 | } 103 | return res.json({ post }); 104 | }) 105 | .catch((err) => res.status(400).send({ 106 | error: err.message 107 | })); 108 | } 109 | 110 | // UppostDate existing post 111 | function updatePost(req, res) { 112 | 113 | const postSlug = req.params.postSlug; 114 | 115 | Post.findOne({where: { postSlug }, include: [{ 116 | model: Term, 117 | as: 'postTerms', 118 | required: false, 119 | attributes: ['id','termType','termName','termSlug'], 120 | through: { attributes: [] } 121 | }] 122 | }) 123 | .then((post) => { 124 | if (!post) { 125 | return res.status(404).send({ 126 | error: 'No post found' 127 | }); 128 | } 129 | 130 | // Update the post slug based on the title if title is new 131 | const postTitle = req.body.postTitle ? req.body.postTitle.trim() : null; 132 | if (req.body.postTitle && !post.dataValues.postSlug.includes(`${changeCase.paramCase(postTitle)}`)) { 133 | let slug = postTitle ? `${changeCase.paramCase(postTitle)}-${Date.now()}` : null; 134 | req.body.postSlug = slug; 135 | } 136 | 137 | let termsPromises = []; 138 | 139 | if (req.body.postTerms) { 140 | post.setPostTerms(); 141 | req.body.postTerms.forEach((term) => { 142 | let { termType, termName } = term; 143 | term.termSlug = `${changeCase.paramCase(termType)}-${changeCase.paramCase(termName)}`; 144 | let termReq = Term.findOrCreate({where: {termSlug: term.termSlug}, defaults: { termType, termName }}) 145 | .spread((term2) => { 146 | post.addPostTerm(term2); 147 | }); 148 | termsPromises.push(termReq); 149 | }); 150 | } 151 | 152 | post.updateAttributes(req.body); 153 | 154 | Promise.all([ 155 | termsPromises 156 | ]) 157 | .then(() => { 158 | return post.save(); 159 | }) 160 | .then((post2) => { 161 | return res.json({ post: post2 }); 162 | }) 163 | .catch((err) => res.status(400).send({ 164 | error: err.message 165 | })); 166 | }) 167 | .catch((err) => res.status(400).send({ 168 | error: err.message 169 | })); 170 | } 171 | 172 | // Delete one post 173 | function deletePost(req, res) { 174 | const postSlug = req.params.postSlug; 175 | Post.findOne({where: { postSlug }}) 176 | .then((post) => { 177 | post.destroy() 178 | .then(() => { 179 | res.status(200).send({ 180 | success: 'Post successfully deleted.' 181 | }); 182 | }); 183 | }) 184 | .catch((err) => res.status(400).send({ 185 | error: err.message 186 | })); 187 | } 188 | 189 | module.exports = { 190 | createPost, 191 | getAllPosts, 192 | getPost, 193 | updatePost, 194 | deletePost 195 | }; 196 | -------------------------------------------------------------------------------- /server/api/post/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const controller = require('./controller'); 3 | const passport = require('passport'); 4 | 5 | router.route('/') 6 | .post(passport.authenticate('jwt', { session: false }), controller.createPost); 7 | 8 | router.route('/') 9 | .get(controller.getAllPosts); 10 | 11 | router.route('/:postSlug') 12 | .get(controller.getPost); 13 | 14 | router.route('/:postSlug') 15 | .patch(passport.authenticate('jwt', { session: false }), controller.updatePost); 16 | 17 | router.route('/:postSlug') 18 | .delete(passport.authenticate('jwt', { session: false }), controller.deletePost); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /server/api/user/controller.js: -------------------------------------------------------------------------------- 1 | // Get one user 2 | function getUser(req, res) { 3 | res.send({user: res.req.user}); 4 | } 5 | 6 | module.exports = { 7 | getUser 8 | }; 9 | -------------------------------------------------------------------------------- /server/api/user/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const controller = require('./controller'); 3 | const passport = require('passport'); 4 | 5 | router.route('/') 6 | .get(passport.authenticate('jwt', { session: false }), controller.getUser); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /server/auth/controller.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const passport = require('passport'); 3 | const config = require('../config/config'); 4 | 5 | const validateEmail = require('../utils/helpers.js').validateEmail; 6 | const validatePassword = require('../utils/helpers.js').validatePassword; 7 | 8 | function signToken(req, res, err, user, info) { 9 | if (err) { 10 | return res.status(400).json({ 11 | error: info.message 12 | }); 13 | } 14 | if (!user) { 15 | return res.status(401).json({ 16 | error: info.message 17 | }); 18 | } 19 | req.login(user, { session: false }, (error) => { 20 | if (error) { 21 | return res.send({error}); 22 | } 23 | // generate a signed son web token with the contents of user object and return it in the response 24 | const token = jwt.sign(user, config.secrets.jwt, { expiresIn: 86400 * 30 }); 25 | // Use to ensure token is valid and debug non-working bearer 26 | // jwt.verify(token, config.secrets.jwt, (errs, data) => { 27 | // console.log(errs, data); 28 | // }); 29 | return res.json({ 30 | user, 31 | message: info.message, 32 | token 33 | }); 34 | }); 35 | } 36 | 37 | function verifyUser(req, res /*, next*/) { 38 | passport.authenticate('local-login', { session: false }, (err, user, info) => { 39 | signToken(req, res, err, user, info); 40 | })(req, res); 41 | } 42 | 43 | // Register new user 44 | function registerUser(req, res) { 45 | const email = req.body.email ? req.body.email.trim() : ''; 46 | const password = req.body.password ? req.body.password.trim() : ''; 47 | 48 | if (!email || !password) { 49 | return res 50 | .status(422) 51 | .send({ error: 'Email, and password are required.' }); 52 | } 53 | 54 | const emailValidationError = validateEmail(email); 55 | if (emailValidationError.length > 0) { 56 | return res 57 | .status(400) 58 | .send({ error: emailValidationError }); // array of errors 59 | } 60 | 61 | const passwordValidationError = validatePassword(password); 62 | if (passwordValidationError.length > 0) { 63 | return res 64 | .status(400) 65 | .send({ error: passwordValidationError }); 66 | } 67 | 68 | passport.authenticate('local-register', { session: false }, (err, user, info) => { 69 | signToken(req, res, err, user, info); 70 | })(req, res); 71 | } 72 | 73 | module.exports = { 74 | verifyUser, 75 | registerUser 76 | }; 77 | -------------------------------------------------------------------------------- /server/auth/passport.js: -------------------------------------------------------------------------------- 1 | const config = require('../config/config'); 2 | 3 | const passport = require('passport'); 4 | const LocalStrategy = require('passport-local').Strategy; 5 | const User = require('../../models').User; 6 | 7 | const passportJWT = require('passport-jwt'); 8 | const JWTStrategy = passportJWT.Strategy; 9 | const ExtractJWT = passportJWT.ExtractJwt; 10 | 11 | const bcrypt = require('bcrypt'); 12 | 13 | passport.use('local-login', new LocalStrategy({ 14 | usernameField: 'email', 15 | passwordField: 'password' 16 | }, 17 | (email, password, cb) => { 18 | // Sequelize will find the user with raw data returned 19 | User.findOne({where: {userEmail: email},raw: true}) 20 | .then((user) => { 21 | if (!user) { 22 | return cb(null, false, { 23 | message: 'Incorrect email or password.' 24 | }); 25 | } 26 | // Don't forget bcrypt as passwords are encrypted 27 | if (!bcrypt.compareSync(password, user.userPass)) { 28 | return cb(null, false, { 29 | message: 'Incorrect email or password.' 30 | }); 31 | } 32 | return cb(null, user, { 33 | message: 'Logged in successfully' 34 | }); 35 | }) 36 | .catch((err) => cb(err)); 37 | } 38 | )); 39 | 40 | passport.use('local-register', new LocalStrategy({ 41 | usernameField: 'email', 42 | passwordField: 'password' 43 | }, (email, password, cb) => { 44 | // Sequelize will find the user with raw data returned 45 | User.findOne({where: {userEmail: email},raw: true}) 46 | .then((user) => { 47 | if (user) { 48 | return cb(null, false, { 49 | message: 'The email is already registered.' 50 | }); 51 | } 52 | const salt = bcrypt.genSaltSync(10); 53 | const hash = bcrypt.hashSync(password, salt); 54 | 55 | const userObj = { 56 | userEmail: email, 57 | userPass: hash 58 | }; 59 | 60 | User.create(userObj) 61 | .then((newUser) => { 62 | const dataObj = newUser.get({plain:true}); 63 | return cb(null, dataObj, { 64 | message: 'User created successfully.' 65 | }); 66 | }) 67 | .catch((err) => { 68 | return cb(err); 69 | }); 70 | }) 71 | .catch((err) => cb(err)); 72 | 73 | })); 74 | 75 | passport.use(new JWTStrategy({ 76 | jwtFromRequest: ExtractJWT.fromAuthHeaderWithScheme('Bearer'), 77 | secretOrKey: config.secrets.jwt 78 | }, 79 | (jwtPayload, cb) => { 80 | // Use the JWT token to find the user in the db if required 81 | User.findOne({where: {userEmail: jwtPayload.userEmail},raw: true}) 82 | .then((user) => { 83 | cb(null, user); 84 | // deal with the promise return via null 85 | return null; 86 | }) 87 | .catch((err) => { 88 | cb(err, null); 89 | 90 | return null; 91 | }); 92 | } 93 | )); 94 | -------------------------------------------------------------------------------- /server/auth/routes.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const controller = require('./controller'); 3 | 4 | router.route('/register') 5 | .post(controller.registerUser); 6 | 7 | router.route('/login') 8 | .post(controller.verifyUser); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | dev: 'development', 3 | test: 'test', 4 | prod: 'production', 5 | port: process.env.PORT || 1337, 6 | expireTime: '7d', 7 | secrets: { 8 | jwt: process.env.JWT || 'labor_risque_pewter_nisei' 9 | } 10 | }; 11 | 12 | // Setting environment variable 13 | process.env.NODE_ENV = process.env.NODE_ENV || config.dev; 14 | config.env = process.env.NODE_ENV; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /server/config/middlewares.js: -------------------------------------------------------------------------------- 1 | // Setup middlewares 2 | const morgan = require('morgan'); 3 | const bodyParser = require('body-parser'); 4 | const compression = require('compression'); 5 | const helmet = require('helmet'); 6 | const cors = require('cors'); 7 | 8 | module.exports = (app) => { 9 | app.use(morgan('dev')); 10 | app.use(compression()); 11 | app.use(bodyParser.json()); 12 | app.use(bodyParser.urlencoded({ 'extended': true })); 13 | app.use(helmet()); 14 | app.use(cors()); 15 | }; 16 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: ["error", { "args": "none" }] */ 2 | 3 | const express = require('express'); 4 | 5 | const app = express(); 6 | const api = require('./api/api'); 7 | const auth = require('./auth/routes'); 8 | 9 | // Middlewares setup 10 | require('./auth/passport'); 11 | require('./config/middlewares')(app); 12 | 13 | // Routes 14 | app.use('/api', api); 15 | app.use('/auth', auth); 16 | 17 | app.use((err, req, res, next) => { 18 | res.status(err.status || 500); 19 | res.json({ error : err }); 20 | }); 21 | 22 | module.exports = app; 23 | -------------------------------------------------------------------------------- /server/utils/helpers.js: -------------------------------------------------------------------------------- 1 | function validateEmail(email) { 2 | let errorMessage = ''; 3 | const regex = /\S+@\S+\.\S+/; 4 | const trimmedEmail = email.trim(); 5 | 6 | if (trimmedEmail.length > 40) { 7 | errorMessage = '* Email is too long, please use shorter email address'; 8 | } 9 | 10 | if (!regex.test(trimmedEmail) || trimmedEmail.length === 0) { 11 | errorMessage = '* Email must be in valid format'; 12 | } 13 | 14 | return errorMessage; 15 | } 16 | 17 | function validatePassword(password) { 18 | 19 | const errorMessages = []; 20 | 21 | if (password.length > 50) { 22 | errorMessages.push('* Must be fewer than 50 chars'); 23 | } 24 | 25 | if (password.length < 8) { 26 | errorMessages.push('* Must be longer than 7 chars'); 27 | } 28 | 29 | return errorMessages; 30 | } 31 | 32 | function validateStringLength(content, limit) { 33 | let errorMessage = ''; 34 | if (content.trim().length > limit) { 35 | errorMessage = `* Cannot be more than ${limit} characters`; 36 | } else if (content.trim().length <= 0) { 37 | errorMessage = '* Cannot be empty'; 38 | } else { 39 | errorMessage = ''; 40 | } 41 | return errorMessage; 42 | } 43 | 44 | module.exports = { 45 | validateEmail, 46 | validatePassword, 47 | validateStringLength 48 | }; 49 | -------------------------------------------------------------------------------- /test/auth/auth.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const app = require('../../server/server'); 4 | const request = require('supertest'); 5 | const expect = require('chai').expect; 6 | const faker = require('faker'); 7 | 8 | describe('[Authentication] /auth Testing', () => { 9 | it('should be able to sign in with correct credentials', (done) => { 10 | request(app) 11 | .post('/auth/register') 12 | .send({ 13 | email: 'user@email.com', 14 | password: 'passwrod' 15 | }) 16 | .set('Accept', 'application/json') 17 | .expect('Content-Type', /json/) 18 | .expect(201) 19 | .end((err, res) => { 20 | request(app) 21 | .post('/auth/signin') 22 | .send({ 23 | email: 'user@email.com', 24 | password: 'passwrod' 25 | }) 26 | .set('Accept', 'application/json') 27 | .expect('Content-Type', /json/) 28 | .expect(200) 29 | .end((err2, res) => { 30 | expect(res.body).to.be.an('object'); 31 | done(); 32 | }); 33 | }); 34 | }); 35 | 36 | it('should not be able to sign in if credentials are incorrect', (done) => { 37 | request(app) 38 | .post('/auth/login') 39 | .send({ 40 | email: 'user@email.com', 41 | password:'BadPass123!' 42 | }) 43 | .set('Accept', 'application/json') 44 | .expect('Content-Type', /json/) 45 | .expect(401) 46 | .end((err, res) => { 47 | expect(res.body).to.be.an('object'); 48 | expect(res.body).to.have.property('error'); 49 | expect(res.body).to.have.deep.property('error', 'Incorrect email or password.'); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('should be able to sign up a new user', (done) => { 55 | const email = faker.internet.email(); 56 | request(app) 57 | .post('/auth/register/') 58 | .send({ 59 | email: email, 60 | password:'Pass123!' 61 | }) 62 | .set('Accept', 'application/json') 63 | .expect('Content-Type', /json/) 64 | .expect(201) 65 | .end((err, res) => { 66 | expect(res.body).to.be.an('object'); 67 | expect(res.body).to.have.property('token'); 68 | expect(res.body).to.have.deep.property('user.userEmail', email); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should not be able to sign up if any inputs are empty', (done) => { 74 | request(app) 75 | .post('/auth/register/') 76 | .send({ 77 | email: null, 78 | password: null 79 | }) 80 | .set('Accept', 'application/json') 81 | .expect('Content-Type', /json/) 82 | .expect(422) 83 | .end((err, res) => { 84 | expect(res.body).to.be.an('object'); 85 | expect(res.body).to.have.property('error'); 86 | expect(res.body).to.have.deep.property('error', 'Email, and password are required.'); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should not be able to sign up a user with same email', (done) => { 92 | request(app) 93 | .post('/auth/register/') 94 | .send({ 95 | email: 'user@email.com', 96 | password: 'Pass123!' 97 | }) 98 | .set('Accept', 'application/json') 99 | .expect('Content-Type', /json/) 100 | .expect(422) 101 | .end((err, res) => { 102 | expect(res.body).to.be.an('object'); 103 | expect(res.body).to.have.property('error'); 104 | expect(res.body).to.have.deep.property('error', 'The email is already registered.'); 105 | done(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /test/post/post.test.js: -------------------------------------------------------------------------------- 1 | const app = require('../../server/server'); 2 | const request = require('supertest'); 3 | const expect = require('chai').expect; 4 | const faker = require('faker'); 5 | const changeCase = require('change-case'); 6 | 7 | describe('[POST] /api/posts Testing', () => { 8 | 9 | let postSlug = '', 10 | token = '', 11 | title = faker.lorem.sentence(5), 12 | postKeys = [ 13 | 'id', 14 | 'postTitle', 15 | 'postSlug', 16 | 'postType', 17 | 'postDate', 18 | 'postContent', 19 | 'postAuthor', 20 | 'postImage', 21 | 'postMedia', 22 | 'postStatus', 23 | 'postExpiry', 24 | 'postFrequency', 25 | 'postTerms', 26 | 'createdAt', 27 | 'updatedAt' 28 | ], 29 | postRequest = { 30 | postTitle: title, 31 | postType: faker.random.arrayElement(['post','page']), 32 | postDate: new Date(), 33 | postContent: faker.lorem.sentences(3,3), 34 | postAuthor: faker.name.findName(), 35 | postImage: faker.image.imageUrl(), 36 | postMedia: faker.image.imageUrl(), 37 | postStatus: faker.random.arrayElement(['published','draft']), 38 | postExpiry: faker.date.future(), 39 | postFrequency: faker.random.arrayElement([null,'day','week','fortnight','month']), 40 | postTerms: [{ 41 | termType: 'tag', 42 | termName: 'fws' 43 | }, { 44 | termType: 'tag', 45 | termName: 'Ho' 46 | }, { 47 | termType: 'category', 48 | termName: 'Ho' 49 | }] 50 | }; 51 | 52 | it('should be able to set login token', (done) => { 53 | request(app) 54 | .post('/auth/login') 55 | .send({ 56 | email: 'user@email.com', 57 | password: 'passwrod' 58 | }) 59 | .set('Accept', 'application/json') 60 | .expect('Content-Type', /json/) 61 | .expect(200) 62 | .end((err, res) => { 63 | expect(res.body).to.be.an('object'); 64 | token = res.body.token; 65 | done(); 66 | }); 67 | }); 68 | 69 | it('should be able to get a list of all seeded posts', (done) => { 70 | request(app) 71 | .get('/api/posts') 72 | .expect(200) 73 | .end((err, res) => { 74 | expect(res.body.posts).to.be.an('array'); 75 | expect(res.body.posts[0]).to.have.all.keys(postKeys); 76 | // set post id for next test 77 | postSlug = res.body.posts[0].postSlug; 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should be able to get a single post', (done) => { 83 | request(app) 84 | .get(`/api/posts/${postSlug}`) 85 | .expect(200) 86 | .end((err, res) => { 87 | expect(res.body.post).to.be.an('object'); 88 | expect(res.body.post).to.have.all.keys(postKeys); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should error with wrong post slug', (done) => { 94 | request(app) 95 | .get('/api/posts/no-post-here') 96 | .expect(400) 97 | .end((err, res) => { 98 | expect(res.body).to.have.property('error'); 99 | expect(res.body).to.have.deep.property('error', 'No post found'); 100 | done(); 101 | }); 102 | }); 103 | 104 | it('should be able to create a post if logged in', (done) => { 105 | request(app) 106 | .post('/api/posts') 107 | .send(postRequest) 108 | .set('Authorization', `Bearer ${token}`) 109 | .set('Accept', 'application/json') 110 | .expect('Content-Type', /json/) 111 | .expect(201) 112 | .end((err, res) => { 113 | expect(res.body.post).to.be.an('object'); 114 | expect(res.body.post).to.have.all.keys(postKeys); 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should be able to update a post if logged in', (done) => { 120 | let updatedPost = postRequest, 121 | newTitle = faker.lorem.sentence(1); 122 | updatedPost.postTitle = newTitle; 123 | request(app) 124 | .patch(`/api/posts/${postSlug}`) 125 | .send(updatedPost) 126 | .set('Authorization', `Bearer ${token}`) 127 | .set('Accept', 'application/json') 128 | .expect('Content-Type', /json/) 129 | .expect(200) 130 | .end((err, res) => { 131 | postSlug = res.body.post.postSlug; 132 | expect(res.body.post).to.be.an('object'); 133 | expect(res.body.post).to.have.all.keys(postKeys); 134 | expect(res.body.post.postSlug).to.include(changeCase.paramCase(newTitle)); 135 | done(); 136 | }); 137 | }); 138 | 139 | it('update should error with wrong post slug', (done) => { 140 | request(app) 141 | .patch('/api/posts/no-post-here') 142 | .send(postRequest) 143 | .set('Authorization', `Bearer ${token}`) 144 | .set('Accept', 'application/json') 145 | .expect('Content-Type', /json/) 146 | .expect(400) 147 | .end((err, res) => { 148 | expect(res.body).to.have.property('error'); 149 | expect(res.body).to.have.deep.property('error', 'No post found'); 150 | done(); 151 | }); 152 | }); 153 | 154 | it('should be able to delete a post if logged in', (done) => { 155 | request(app) 156 | .delete(`/api/posts/${postSlug}`) 157 | .set('Authorization', `Bearer ${token}`) 158 | .set('Accept', 'application/json') 159 | .expect('Content-Type', /json/) 160 | .expect(202) 161 | .end((err, res) => { 162 | expect(res.body).to.be.an('object'); 163 | expect(res.body).to.have.property('success'); 164 | expect(res.body).to.have.deep.property('success', 'Post successfully deleted.'); 165 | done(); 166 | }); 167 | }); 168 | 169 | it('should error with wrong delete post slug', (done) => { 170 | request(app) 171 | .get(`/api/posts/${postSlug}`) 172 | .expect(400) 173 | .end((err, res) => { 174 | expect(res.body).to.have.property('error'); 175 | expect(res.body).to.have.deep.property('error', 'No post found'); 176 | done(); 177 | }); 178 | }); 179 | 180 | it('it should reject post with no title', (done) => { 181 | request(app) 182 | .post('/api/posts') 183 | .send({}) 184 | .set('Authorization', `Bearer ${token}`) 185 | .set('Accept', 'application/json') 186 | .expect('Content-Type', /json/) 187 | .expect(422) 188 | .end((err, res) => { 189 | expect(res.body).to.have.property('error'); 190 | expect(res.body).to.have.deep.property('error', 'A postTitle is required.'); 191 | done(); 192 | }); 193 | }); 194 | 195 | it('it should reject post with no term name', (done) => { 196 | let noTermName = postRequest; 197 | noTermName.postTerms = [{ 198 | termType: 'tag', 199 | termName: 'fws' 200 | }, { 201 | termType: 'tag' 202 | }, { 203 | termType: 'category', 204 | termName: 'Ho' 205 | }]; 206 | request(app) 207 | .post('/api/posts') 208 | .send(noTermName) 209 | .set('Authorization', `Bearer ${token}`) 210 | .set('Accept', 'application/json') 211 | .expect('Content-Type', /json/) 212 | .expect(422) 213 | .end((err, res) => { 214 | expect(res.body).to.have.property('error'); 215 | expect(res.body).to.have.deep.property('error', 'All terms require a termType and termName.'); 216 | done(); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /test/seedpress-api.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "f97e2d84-9654-400b-b368-a05933b11a6c", 4 | "name": "seedpress-api", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 6 | }, 7 | "item": [ 8 | { 9 | "name": "auth", 10 | "description": "", 11 | "item": [ 12 | { 13 | "name": "localhost: 1337/auth/register", 14 | "request": { 15 | "method": "POST", 16 | "header": [ 17 | { 18 | "key": "Content-Type", 19 | "value": "application/json" 20 | } 21 | ], 22 | "body": { 23 | "mode": "raw", 24 | "raw": "{\"email\":\"new@user.com\",\"password\":\"passwrod\"}" 25 | }, 26 | "url": { 27 | "raw": "localhost:1337/auth/register", 28 | "host": [ 29 | "localhost" 30 | ], 31 | "port": "1337", 32 | "path": [ 33 | "auth", 34 | "register" 35 | ] 36 | } 37 | }, 38 | "response": [ 39 | { 40 | "id": "232f24f1-1a13-4cc1-b4ea-82b567b92a0d", 41 | "name": "localhost: 1337/auth/register", 42 | "originalRequest": { 43 | "method": "POST", 44 | "header": [ 45 | { 46 | "key": "Content-Type", 47 | "value": "application/json", 48 | "disabled": false 49 | } 50 | ], 51 | "body": { 52 | "mode": "raw", 53 | "raw": "{\"email\":\"new@user.com\",\"password\":\"passwrod\"}" 54 | }, 55 | "url": { 56 | "raw": "localhost:1337/auth/register", 57 | "host": [ 58 | "localhost" 59 | ], 60 | "port": "1337", 61 | "path": [ 62 | "auth", 63 | "register" 64 | ] 65 | } 66 | }, 67 | "status": "OK", 68 | "code": 200, 69 | "_postman_previewlanguage": "json", 70 | "header": [ 71 | { 72 | "key": "Access-Control-Allow-Origin", 73 | "value": "*", 74 | "name": "Access-Control-Allow-Origin", 75 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 76 | }, 77 | { 78 | "key": "Connection", 79 | "value": "keep-alive", 80 | "name": "Connection", 81 | "description": "Options that are desired for the connection" 82 | }, 83 | { 84 | "key": "Content-Length", 85 | "value": "776", 86 | "name": "Content-Length", 87 | "description": "The length of the response body in octets (8-bit bytes)" 88 | }, 89 | { 90 | "key": "Content-Type", 91 | "value": "application/json; charset=utf-8", 92 | "name": "Content-Type", 93 | "description": "The mime type of this content" 94 | }, 95 | { 96 | "key": "Date", 97 | "value": "Mon, 16 Jul 2018 04:53:21 GMT", 98 | "name": "Date", 99 | "description": "The date and time that the message was sent" 100 | }, 101 | { 102 | "key": "ETag", 103 | "value": "W/\"308-i+MiB35zBWmyyynQxm0G0r/hiWA\"", 104 | "name": "ETag", 105 | "description": "An identifier for a specific version of a resource, often a message digest" 106 | }, 107 | { 108 | "key": "Strict-Transport-Security", 109 | "value": "max-age=15552000; includeSubDomains", 110 | "name": "Strict-Transport-Security", 111 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 112 | }, 113 | { 114 | "key": "Vary", 115 | "value": "Accept-Encoding", 116 | "name": "Vary", 117 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 118 | }, 119 | { 120 | "key": "X-Content-Type-Options", 121 | "value": "nosniff", 122 | "name": "X-Content-Type-Options", 123 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 124 | }, 125 | { 126 | "key": "X-DNS-Prefetch-Control", 127 | "value": "off", 128 | "name": "X-DNS-Prefetch-Control", 129 | "description": "Custom header" 130 | }, 131 | { 132 | "key": "X-Download-Options", 133 | "value": "noopen", 134 | "name": "X-Download-Options", 135 | "description": "Custom header" 136 | }, 137 | { 138 | "key": "X-Frame-Options", 139 | "value": "SAMEORIGIN", 140 | "name": "X-Frame-Options", 141 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 142 | }, 143 | { 144 | "key": "X-XSS-Protection", 145 | "value": "1; mode=block", 146 | "name": "X-XSS-Protection", 147 | "description": "Cross-site scripting (XSS) filter" 148 | } 149 | ], 150 | "cookie": [], 151 | "body": "{\"user\":{\"id\":\"aa305d9d-eb35-4fd2-81fe-023edb828430\",\"email\":\"new@user.com\",\"password\":\"$2b$10$F9sE70763SGtpc4j7V5Qhuo.nPmrWbTpWjGC7ZKwFzpX.S44yuSke\",\"updatedAt\":\"2018-07-16T04:53:21.647Z\",\"createdAt\":\"2018-07-16T04:53:21.647Z\",\"firstName\":null,\"lastName\":null},\"message\":\"User created successfully.\",\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImFhMzA1ZDlkLWViMzUtNGZkMi04MWZlLTAyM2VkYjgyODQzMCIsImVtYWlsIjoibmV3QHVzZXIuY29tIiwicGFzc3dvcmQiOiIkMmIkMTAkRjlzRTcwNzYzU0d0cGM0ajdWNVFodW8ublBtcldiVHBXakdDN1pLd0Z6cFguUzQ0eXVTa2UiLCJ1cGRhdGVkQXQiOiIyMDE4LTA3LTE2VDA0OjUzOjIxLjY0N1oiLCJjcmVhdGVkQXQiOiIyMDE4LTA3LTE2VDA0OjUzOjIxLjY0N1oiLCJmaXJzdE5hbWUiOm51bGwsImxhc3ROYW1lIjpudWxsLCJpYXQiOjE1MzE3MTY4MDEsImV4cCI6MTUzNDMwODgwMX0.RnhNEsgRwAA329dfYTAb2zumjDTps1BWS-wsJqWiYoc\"}" 152 | } 153 | ] 154 | }, 155 | { 156 | "name": "localhost: 1337/auth/login/", 157 | "request": { 158 | "method": "POST", 159 | "header": [ 160 | { 161 | "key": "Content-Type", 162 | "value": "application/json" 163 | } 164 | ], 165 | "body": { 166 | "mode": "raw", 167 | "raw": "{\"email\":\"user@email.com\",\"password\":\"passwrod\"}" 168 | }, 169 | "url": { 170 | "raw": "localhost:1337/auth/login", 171 | "host": [ 172 | "localhost" 173 | ], 174 | "port": "1337", 175 | "path": [ 176 | "auth", 177 | "login" 178 | ] 179 | } 180 | }, 181 | "response": [ 182 | { 183 | "id": "3f93c69f-b3f8-4b3f-83f1-dadd14e78ab7", 184 | "name": "localhost: 1337/auth/login/", 185 | "originalRequest": { 186 | "method": "POST", 187 | "header": [ 188 | { 189 | "key": "Content-Type", 190 | "value": "application/json", 191 | "disabled": false 192 | } 193 | ], 194 | "body": { 195 | "mode": "raw", 196 | "raw": "{\"email\":\"user@email.com\",\"password\":\"passwrod\"}" 197 | }, 198 | "url": { 199 | "raw": "localhost:1337/auth/login", 200 | "host": [ 201 | "localhost" 202 | ], 203 | "port": "1337", 204 | "path": [ 205 | "auth", 206 | "login" 207 | ] 208 | } 209 | }, 210 | "status": "OK", 211 | "code": 200, 212 | "_postman_previewlanguage": "json", 213 | "header": [ 214 | { 215 | "key": "Access-Control-Allow-Origin", 216 | "value": "*", 217 | "name": "Access-Control-Allow-Origin", 218 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 219 | }, 220 | { 221 | "key": "Connection", 222 | "value": "keep-alive", 223 | "name": "Connection", 224 | "description": "Options that are desired for the connection" 225 | }, 226 | { 227 | "key": "Content-Length", 228 | "value": "786", 229 | "name": "Content-Length", 230 | "description": "The length of the response body in octets (8-bit bytes)" 231 | }, 232 | { 233 | "key": "Content-Type", 234 | "value": "application/json; charset=utf-8", 235 | "name": "Content-Type", 236 | "description": "The mime type of this content" 237 | }, 238 | { 239 | "key": "Date", 240 | "value": "Mon, 16 Jul 2018 04:49:16 GMT", 241 | "name": "Date", 242 | "description": "The date and time that the message was sent" 243 | }, 244 | { 245 | "key": "ETag", 246 | "value": "W/\"312-MnA2Kb1b2Y90PNhcT4kiNbko3Y8\"", 247 | "name": "ETag", 248 | "description": "An identifier for a specific version of a resource, often a message digest" 249 | }, 250 | { 251 | "key": "Strict-Transport-Security", 252 | "value": "max-age=15552000; includeSubDomains", 253 | "name": "Strict-Transport-Security", 254 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 255 | }, 256 | { 257 | "key": "Vary", 258 | "value": "Accept-Encoding", 259 | "name": "Vary", 260 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 261 | }, 262 | { 263 | "key": "X-Content-Type-Options", 264 | "value": "nosniff", 265 | "name": "X-Content-Type-Options", 266 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 267 | }, 268 | { 269 | "key": "X-DNS-Prefetch-Control", 270 | "value": "off", 271 | "name": "X-DNS-Prefetch-Control", 272 | "description": "Custom header" 273 | }, 274 | { 275 | "key": "X-Download-Options", 276 | "value": "noopen", 277 | "name": "X-Download-Options", 278 | "description": "Custom header" 279 | }, 280 | { 281 | "key": "X-Frame-Options", 282 | "value": "SAMEORIGIN", 283 | "name": "X-Frame-Options", 284 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 285 | }, 286 | { 287 | "key": "X-XSS-Protection", 288 | "value": "1; mode=block", 289 | "name": "X-XSS-Protection", 290 | "description": "Cross-site scripting (XSS) filter" 291 | } 292 | ], 293 | "cookie": [], 294 | "body": "{\"user\":{\"id\":\"078b0d47-aad2-416a-8b46-5eafbe9c8d91\",\"firstName\":\"John\",\"lastName\":\"User\",\"email\":\"user@email.com\",\"password\":\"$2b$10$dC4GbBR9fmVF9xK6vth/zO.VQqinUoGF8hx6.mdbA/tGuEjKrl/u2\",\"createdAt\":\"2018-07-16T04:44:51.309Z\",\"updatedAt\":\"2018-07-16T04:44:51.309Z\"},\"message\":\"Logged in successfully\",\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjA3OGIwZDQ3LWFhZDItNDE2YS04YjQ2LTVlYWZiZTljOGQ5MSIsImZpcnN0TmFtZSI6IkpvaG4iLCJsYXN0TmFtZSI6IlVzZXIiLCJlbWFpbCI6InVzZXJAZW1haWwuY29tIiwicGFzc3dvcmQiOiIkMmIkMTAkZEM0R2JCUjlmbVZGOXhLNnZ0aC96Ty5WUXFpblVvR0Y4aHg2Lm1kYkEvdEd1RWpLcmwvdTIiLCJjcmVhdGVkQXQiOiIyMDE4LTA3LTE2VDA0OjQ0OjUxLjMwOVoiLCJ1cGRhdGVkQXQiOiIyMDE4LTA3LTE2VDA0OjQ0OjUxLjMwOVoiLCJpYXQiOjE1MzE3MTY1NTYsImV4cCI6MTUzNDMwODU1Nn0.alJ1-EjEr-v9B3ASLE0mwFu9qttGTk0EW5iUe3T_d-M\"}" 295 | } 296 | ] 297 | } 298 | ] 299 | }, 300 | { 301 | "name": "posts", 302 | "description": "", 303 | "item": [ 304 | { 305 | "name": "localhost:1337/api/posts", 306 | "request": { 307 | "method": "POST", 308 | "header": [ 309 | { 310 | "key": "Content-Type", 311 | "value": "application/json" 312 | }, 313 | { 314 | "key": "Authorization", 315 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI" 316 | } 317 | ], 318 | "body": { 319 | "mode": "raw", 320 | "raw": "{\"postTitle\": \"A new post\",\"postType\": \"post\",\"postDate\": \"2018-07-02T00:36:26.371Z\",\"postContent\": \"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\": \"published\",\"postExpiry\": \"2019-05-16T16:49:00.638Z\",\"postTerms\":[{\"termType\":\"tag\",\"termName\":\"fws\"},{\"termType\":\"tag\",\"termName\":\"Ho\"}]}" 321 | }, 322 | "url": { 323 | "raw": "localhost:1337/api/posts", 324 | "host": [ 325 | "localhost" 326 | ], 327 | "port": "1337", 328 | "path": [ 329 | "api", 330 | "posts" 331 | ] 332 | } 333 | }, 334 | "response": [ 335 | { 336 | "id": "dcf60ef0-0cf5-436f-ba1c-77a84d1b003a", 337 | "name": "localhost:1337/api/posts", 338 | "originalRequest": { 339 | "method": "POST", 340 | "header": [ 341 | { 342 | "key": "Content-Type", 343 | "value": "application/json", 344 | "disabled": false 345 | }, 346 | { 347 | "key": "Authorization", 348 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI", 349 | "disabled": false 350 | } 351 | ], 352 | "body": { 353 | "mode": "raw", 354 | "raw": "{\"postTitle\": \"A new post\",\"postType\": \"post\",\"postDate\": \"2018-07-02T00:36:26.371Z\",\"postContent\": \"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\": \"published\",\"postExpiry\": \"2019-05-16T16:49:00.638Z\",\"postTerms\":[{\"termType\":\"tag\",\"termName\":\"fws\"},{\"termType\":\"tag\",\"termName\":\"Ho\"}]}" 355 | }, 356 | "url": { 357 | "raw": "localhost:1337/api/posts", 358 | "host": [ 359 | "localhost" 360 | ], 361 | "port": "1337", 362 | "path": [ 363 | "api", 364 | "posts" 365 | ] 366 | } 367 | }, 368 | "status": "OK", 369 | "code": 200, 370 | "_postman_previewlanguage": "json", 371 | "header": [ 372 | { 373 | "key": "Access-Control-Allow-Origin", 374 | "value": "*", 375 | "name": "Access-Control-Allow-Origin", 376 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 377 | }, 378 | { 379 | "key": "Connection", 380 | "value": "keep-alive", 381 | "name": "Connection", 382 | "description": "Options that are desired for the connection" 383 | }, 384 | { 385 | "key": "Content-Encoding", 386 | "value": "gzip", 387 | "name": "Content-Encoding", 388 | "description": "The type of encoding used on the data." 389 | }, 390 | { 391 | "key": "Content-Type", 392 | "value": "application/json; charset=utf-8", 393 | "name": "Content-Type", 394 | "description": "The mime type of this content" 395 | }, 396 | { 397 | "key": "Date", 398 | "value": "Mon, 23 Jul 2018 04:16:54 GMT", 399 | "name": "Date", 400 | "description": "The date and time that the message was sent" 401 | }, 402 | { 403 | "key": "ETag", 404 | "value": "W/\"406-W9u+n1QRvBEUwEE/t+0Y9/uEgh4\"", 405 | "name": "ETag", 406 | "description": "An identifier for a specific version of a resource, often a message digest" 407 | }, 408 | { 409 | "key": "Strict-Transport-Security", 410 | "value": "max-age=15552000; includeSubDomains", 411 | "name": "Strict-Transport-Security", 412 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 413 | }, 414 | { 415 | "key": "Transfer-Encoding", 416 | "value": "chunked", 417 | "name": "Transfer-Encoding", 418 | "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." 419 | }, 420 | { 421 | "key": "Vary", 422 | "value": "Accept-Encoding", 423 | "name": "Vary", 424 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 425 | }, 426 | { 427 | "key": "X-Content-Type-Options", 428 | "value": "nosniff", 429 | "name": "X-Content-Type-Options", 430 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 431 | }, 432 | { 433 | "key": "X-DNS-Prefetch-Control", 434 | "value": "off", 435 | "name": "X-DNS-Prefetch-Control", 436 | "description": "Custom header" 437 | }, 438 | { 439 | "key": "X-Download-Options", 440 | "value": "noopen", 441 | "name": "X-Download-Options", 442 | "description": "Custom header" 443 | }, 444 | { 445 | "key": "X-Frame-Options", 446 | "value": "SAMEORIGIN", 447 | "name": "X-Frame-Options", 448 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 449 | }, 450 | { 451 | "key": "X-XSS-Protection", 452 | "value": "1; mode=block", 453 | "name": "X-XSS-Protection", 454 | "description": "Cross-site scripting (XSS) filter" 455 | } 456 | ], 457 | "cookie": [], 458 | "body": "{\"post\":{\"id\":1,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532319414478\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"updatedAt\":\"2018-07-23T04:16:54.480Z\",\"createdAt\":\"2018-07-23T04:16:54.480Z\",\"postTerms\":[{\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]}}" 459 | } 460 | ] 461 | }, 462 | { 463 | "name": "localhost:1337/api/posts", 464 | "request": { 465 | "method": "GET", 466 | "header": [], 467 | "body": {}, 468 | "url": { 469 | "raw": "localhost:1337/api/posts", 470 | "host": [ 471 | "localhost" 472 | ], 473 | "port": "1337", 474 | "path": [ 475 | "api", 476 | "posts" 477 | ] 478 | } 479 | }, 480 | "response": [ 481 | { 482 | "id": "fd1ded84-6de0-405a-9fd9-55bea7e44370", 483 | "name": "localhost:1337/api/posts", 484 | "originalRequest": { 485 | "method": "GET", 486 | "header": [], 487 | "body": {}, 488 | "url": { 489 | "raw": "localhost:1337/api/posts", 490 | "host": [ 491 | "localhost" 492 | ], 493 | "port": "1337", 494 | "path": [ 495 | "api", 496 | "posts" 497 | ] 498 | } 499 | }, 500 | "status": "OK", 501 | "code": 200, 502 | "_postman_previewlanguage": "json", 503 | "header": [ 504 | { 505 | "key": "Access-Control-Allow-Origin", 506 | "value": "*", 507 | "name": "Access-Control-Allow-Origin", 508 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 509 | }, 510 | { 511 | "key": "Connection", 512 | "value": "keep-alive", 513 | "name": "Connection", 514 | "description": "Options that are desired for the connection" 515 | }, 516 | { 517 | "key": "Content-Encoding", 518 | "value": "gzip", 519 | "name": "Content-Encoding", 520 | "description": "The type of encoding used on the data." 521 | }, 522 | { 523 | "key": "Content-Type", 524 | "value": "application/json; charset=utf-8", 525 | "name": "Content-Type", 526 | "description": "The mime type of this content" 527 | }, 528 | { 529 | "key": "Date", 530 | "value": "Mon, 23 Jul 2018 03:01:48 GMT", 531 | "name": "Date", 532 | "description": "The date and time that the message was sent" 533 | }, 534 | { 535 | "key": "ETag", 536 | "value": "W/\"3292-xdLPtorrPp8BrBKyuFhEtkwTRGk\"", 537 | "name": "ETag", 538 | "description": "An identifier for a specific version of a resource, often a message digest" 539 | }, 540 | { 541 | "key": "Strict-Transport-Security", 542 | "value": "max-age=15552000; includeSubDomains", 543 | "name": "Strict-Transport-Security", 544 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 545 | }, 546 | { 547 | "key": "Transfer-Encoding", 548 | "value": "chunked", 549 | "name": "Transfer-Encoding", 550 | "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." 551 | }, 552 | { 553 | "key": "Vary", 554 | "value": "Accept-Encoding", 555 | "name": "Vary", 556 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 557 | }, 558 | { 559 | "key": "X-Content-Type-Options", 560 | "value": "nosniff", 561 | "name": "X-Content-Type-Options", 562 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 563 | }, 564 | { 565 | "key": "X-DNS-Prefetch-Control", 566 | "value": "off", 567 | "name": "X-DNS-Prefetch-Control", 568 | "description": "Custom header" 569 | }, 570 | { 571 | "key": "X-Download-Options", 572 | "value": "noopen", 573 | "name": "X-Download-Options", 574 | "description": "Custom header" 575 | }, 576 | { 577 | "key": "X-Frame-Options", 578 | "value": "SAMEORIGIN", 579 | "name": "X-Frame-Options", 580 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 581 | }, 582 | { 583 | "key": "X-XSS-Protection", 584 | "value": "1; mode=block", 585 | "name": "X-XSS-Protection", 586 | "description": "Cross-site scripting (XSS) filter" 587 | } 588 | ], 589 | "cookie": [], 590 | "body": "{\"posts\":[{\"id\":3,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314507359\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T02:55:07.361Z\",\"updatedAt\":\"2018-07-23T02:55:07.361Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":4,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314625998\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T02:57:06.000Z\",\"updatedAt\":\"2018-07-23T02:57:06.000Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":5,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314698450\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T02:58:18.452Z\",\"updatedAt\":\"2018-07-23T02:58:18.452Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":6,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314808493\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T03:00:08.495Z\",\"updatedAt\":\"2018-07-23T03:00:08.495Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":7,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314846653\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T03:00:46.655Z\",\"updatedAt\":\"2018-07-23T03:00:46.655Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":8,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314885420\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T03:01:25.421Z\",\"updatedAt\":\"2018-07-23T03:01:25.421Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":9,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314902103\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T03:01:42.105Z\",\"updatedAt\":\"2018-07-23T03:01:42.105Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]},{\"id\":78599,\"postTitle\":\"Velit libero voluptatem repellendus praesentium.\",\"postSlug\":\"velit-libero-voluptatem-repellendus-praesentium-1532314385532\",\"postType\":\"post\",\"postDate\":\"2018-07-23T02:53:05.532Z\",\"postContent\":\"Est quidem dolores repudiandae at magni suscipit corrupti quia dolore.3Dolore aperiam id iure quam asperiores.3Animi odit exercitationem est ducimus voluptatem officiis est nesciunt saepe.\",\"postAuthor\":\"Kamron Daniel\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"draft\",\"postExpiry\":\"2018-07-29T18:36:13.864Z\",\"postFrequency\":\"day\",\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]},{\"id\":90316,\"postTitle\":\"Debitis quos odio mollitia quo.\",\"postSlug\":\"debitis-quos-odio-mollitia-quo-1532314385532\",\"postType\":\"page\",\"postDate\":\"2018-07-23T02:53:05.532Z\",\"postContent\":\"Aut cupiditate consectetur quod rerum distinctio.3Corrupti rerum et laudantium ad itaque sint.3Est qui et pariatur deleniti laudantium.\",\"postAuthor\":\"Kailyn Muller Jr.\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"published\",\"postExpiry\":\"2018-11-01T05:16:57.974Z\",\"postFrequency\":\"fortnight\",\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]},{\"id\":40194,\"postTitle\":\"Et sunt provident voluptas et.\",\"postSlug\":\"et-sunt-provident-voluptas-et-1532314385532\",\"postType\":\"post\",\"postDate\":\"2018-07-23T02:53:05.532Z\",\"postContent\":\"Molestias magnam consequatur quis voluptas debitis et.3Reprehenderit enim culpa qui beatae sed impedit nihil.3Iste numquam et repudiandae velit odit commodi labore quidem.\",\"postAuthor\":\"Jeremie Gottlieb\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"draft\",\"postExpiry\":\"2018-08-26T21:45:38.271Z\",\"postFrequency\":\"fortnight\",\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]},{\"id\":94383,\"postTitle\":\"Blanditiis ea natus quam repudiandae.\",\"postSlug\":\"blanditiis-ea-natus-quam-repudiandae-1532314385531\",\"postType\":\"page\",\"postDate\":\"2018-07-23T02:53:05.531Z\",\"postContent\":\"Nulla ut dolor.3Occaecati facilis aut.3Exercitationem qui illo ducimus veritatis omnis accusantium.\",\"postAuthor\":\"Audra Fisher IV\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"draft\",\"postExpiry\":\"2018-12-30T20:10:47.859Z\",\"postFrequency\":null,\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]},{\"id\":2,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314450730\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T02:54:10.730Z\",\"updatedAt\":\"2018-07-23T02:54:10.730Z\",\"postTerms\":[]},{\"id\":28749,\"postTitle\":\"Nulla suscipit qui animi labore.\",\"postSlug\":\"nulla-suscipit-qui-animi-labore-1532314385532\",\"postType\":\"page\",\"postDate\":\"2018-07-23T02:53:05.532Z\",\"postContent\":\"Recusandae illum soluta et nihil est eos quia quis.3Rerum rem aut inventore deleniti accusamus veritatis architecto id voluptas.3Sed molestias error iusto vero.\",\"postAuthor\":\"Etha Cormier\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"published\",\"postExpiry\":\"2019-01-09T13:14:09.794Z\",\"postFrequency\":null,\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]},{\"id\":1,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532314391936\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T02:53:11.938Z\",\"updatedAt\":\"2018-07-23T02:53:11.938Z\",\"postTerms\":[]},{\"id\":48459,\"postTitle\":\"Aperiam ratione iste consequatur id.\",\"postSlug\":\"aperiam-ratione-iste-consequatur-id-1532314385532\",\"postType\":\"post\",\"postDate\":\"2018-07-23T02:53:05.532Z\",\"postContent\":\"Rerum aliquid ipsam laborum vel.3Facilis eaque temporibus.3Quis nesciunt rerum aspernatur commodi et nemo modi et tempora.\",\"postAuthor\":\"Barton O'Hara\",\"postImage\":\"http://lorempixel.com/640/480\",\"postMedia\":\"http://lorempixel.com/640/480\",\"postStatus\":\"draft\",\"postExpiry\":\"2018-11-17T05:30:11.263Z\",\"postFrequency\":\"fortnight\",\"createdAt\":\"2018-07-23T02:53:05.532Z\",\"updatedAt\":\"2018-07-23T02:53:05.532Z\",\"postTerms\":[]}]}" 591 | } 592 | ] 593 | }, 594 | { 595 | "name": "localhost:1337/api/posts/get-single", 596 | "request": { 597 | "method": "GET", 598 | "header": [], 599 | "body": {}, 600 | "url": { 601 | "raw": "localhost:1337/api/posts/a-new-post-1532319459574", 602 | "host": [ 603 | "localhost" 604 | ], 605 | "port": "1337", 606 | "path": [ 607 | "api", 608 | "posts", 609 | "a-new-post-1532319459574" 610 | ] 611 | } 612 | }, 613 | "response": [ 614 | { 615 | "id": "2be941a0-97be-424f-a259-f3fcc44a0490", 616 | "name": "localhost:1337/api/posts/get-single", 617 | "originalRequest": { 618 | "method": "GET", 619 | "header": [], 620 | "body": {}, 621 | "url": { 622 | "raw": "localhost:1337/api/posts/a-new-post-1532319459574", 623 | "host": [ 624 | "localhost" 625 | ], 626 | "port": "1337", 627 | "path": [ 628 | "api", 629 | "posts", 630 | "a-new-post-1532319459574" 631 | ] 632 | } 633 | }, 634 | "status": "OK", 635 | "code": 200, 636 | "_postman_previewlanguage": "json", 637 | "header": [ 638 | { 639 | "key": "Access-Control-Allow-Origin", 640 | "value": "*", 641 | "name": "Access-Control-Allow-Origin", 642 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 643 | }, 644 | { 645 | "key": "Connection", 646 | "value": "keep-alive", 647 | "name": "Connection", 648 | "description": "Options that are desired for the connection" 649 | }, 650 | { 651 | "key": "Content-Encoding", 652 | "value": "gzip", 653 | "name": "Content-Encoding", 654 | "description": "The type of encoding used on the data." 655 | }, 656 | { 657 | "key": "Content-Type", 658 | "value": "application/json; charset=utf-8", 659 | "name": "Content-Type", 660 | "description": "The mime type of this content" 661 | }, 662 | { 663 | "key": "Date", 664 | "value": "Mon, 23 Jul 2018 04:17:50 GMT", 665 | "name": "Date", 666 | "description": "The date and time that the message was sent" 667 | }, 668 | { 669 | "key": "ETag", 670 | "value": "W/\"414-QVDHZKEdrf2mpxWGi/C9cpyngEM\"", 671 | "name": "ETag", 672 | "description": "An identifier for a specific version of a resource, often a message digest" 673 | }, 674 | { 675 | "key": "Strict-Transport-Security", 676 | "value": "max-age=15552000; includeSubDomains", 677 | "name": "Strict-Transport-Security", 678 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 679 | }, 680 | { 681 | "key": "Transfer-Encoding", 682 | "value": "chunked", 683 | "name": "Transfer-Encoding", 684 | "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." 685 | }, 686 | { 687 | "key": "Vary", 688 | "value": "Accept-Encoding", 689 | "name": "Vary", 690 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 691 | }, 692 | { 693 | "key": "X-Content-Type-Options", 694 | "value": "nosniff", 695 | "name": "X-Content-Type-Options", 696 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 697 | }, 698 | { 699 | "key": "X-DNS-Prefetch-Control", 700 | "value": "off", 701 | "name": "X-DNS-Prefetch-Control", 702 | "description": "Custom header" 703 | }, 704 | { 705 | "key": "X-Download-Options", 706 | "value": "noopen", 707 | "name": "X-Download-Options", 708 | "description": "Custom header" 709 | }, 710 | { 711 | "key": "X-Frame-Options", 712 | "value": "SAMEORIGIN", 713 | "name": "X-Frame-Options", 714 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 715 | }, 716 | { 717 | "key": "X-XSS-Protection", 718 | "value": "1; mode=block", 719 | "name": "X-XSS-Protection", 720 | "description": "Cross-site scripting (XSS) filter" 721 | } 722 | ], 723 | "cookie": [], 724 | "body": "{\"post\":{\"id\":2,\"postTitle\":\"A new post\",\"postSlug\":\"a-new-post-1532319459574\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T04:17:39.574Z\",\"updatedAt\":\"2018-07-23T04:17:39.574Z\",\"postTerms\":[{\"id\":1,\"termType\":\"tag\",\"termName\":\"fws\",\"termSlug\":\"tag-fws\"},{\"id\":2,\"termType\":\"tag\",\"termName\":\"Ho\",\"termSlug\":\"tag-ho\"}]}}" 725 | } 726 | ] 727 | }, 728 | { 729 | "name": "localhost:1337/api/posts/update", 730 | "request": { 731 | "method": "PATCH", 732 | "header": [ 733 | { 734 | "key": "Content-Type", 735 | "value": "application/json" 736 | }, 737 | { 738 | "key": "Authorization", 739 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI" 740 | } 741 | ], 742 | "body": { 743 | "mode": "raw", 744 | "raw": "{\"postTitle\": \"A goodsss day\"}" 745 | }, 746 | "url": { 747 | "raw": "localhost:1337/api/posts/a-new-post-1532319459574", 748 | "host": [ 749 | "localhost" 750 | ], 751 | "port": "1337", 752 | "path": [ 753 | "api", 754 | "posts", 755 | "a-new-post-1532319459574" 756 | ] 757 | } 758 | }, 759 | "response": [ 760 | { 761 | "id": "58bcf9df-5460-4ed3-bc20-7d18a4581b0d", 762 | "name": "localhost:1337/api/posts/update", 763 | "originalRequest": { 764 | "method": "PATCH", 765 | "header": [ 766 | { 767 | "key": "Content-Type", 768 | "value": "application/json", 769 | "disabled": false 770 | }, 771 | { 772 | "key": "Authorization", 773 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI", 774 | "disabled": false 775 | } 776 | ], 777 | "body": { 778 | "mode": "raw", 779 | "raw": "{\"postTitle\": \"A goodsss day\"}" 780 | }, 781 | "url": { 782 | "raw": "localhost:1337/api/posts/a-new-post-1532319459574", 783 | "host": [ 784 | "localhost" 785 | ], 786 | "port": "1337", 787 | "path": [ 788 | "api", 789 | "posts", 790 | "a-new-post-1532319459574" 791 | ] 792 | } 793 | }, 794 | "status": "OK", 795 | "code": 200, 796 | "_postman_previewlanguage": "json", 797 | "header": [ 798 | { 799 | "key": "Access-Control-Allow-Origin", 800 | "value": "*", 801 | "name": "Access-Control-Allow-Origin", 802 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 803 | }, 804 | { 805 | "key": "Connection", 806 | "value": "keep-alive", 807 | "name": "Connection", 808 | "description": "Options that are desired for the connection" 809 | }, 810 | { 811 | "key": "Content-Length", 812 | "value": "901", 813 | "name": "Content-Length", 814 | "description": "The length of the response body in octets (8-bit bytes)" 815 | }, 816 | { 817 | "key": "Content-Type", 818 | "value": "application/json; charset=utf-8", 819 | "name": "Content-Type", 820 | "description": "The mime type of this content" 821 | }, 822 | { 823 | "key": "Date", 824 | "value": "Mon, 23 Jul 2018 04:19:05 GMT", 825 | "name": "Date", 826 | "description": "The date and time that the message was sent" 827 | }, 828 | { 829 | "key": "ETag", 830 | "value": "W/\"385-/8/0KZuutPPJMxkE5JHarcUf3gA\"", 831 | "name": "ETag", 832 | "description": "An identifier for a specific version of a resource, often a message digest" 833 | }, 834 | { 835 | "key": "Strict-Transport-Security", 836 | "value": "max-age=15552000; includeSubDomains", 837 | "name": "Strict-Transport-Security", 838 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 839 | }, 840 | { 841 | "key": "Vary", 842 | "value": "Accept-Encoding", 843 | "name": "Vary", 844 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 845 | }, 846 | { 847 | "key": "X-Content-Type-Options", 848 | "value": "nosniff", 849 | "name": "X-Content-Type-Options", 850 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 851 | }, 852 | { 853 | "key": "X-DNS-Prefetch-Control", 854 | "value": "off", 855 | "name": "X-DNS-Prefetch-Control", 856 | "description": "Custom header" 857 | }, 858 | { 859 | "key": "X-Download-Options", 860 | "value": "noopen", 861 | "name": "X-Download-Options", 862 | "description": "Custom header" 863 | }, 864 | { 865 | "key": "X-Frame-Options", 866 | "value": "SAMEORIGIN", 867 | "name": "X-Frame-Options", 868 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 869 | }, 870 | { 871 | "key": "X-XSS-Protection", 872 | "value": "1; mode=block", 873 | "name": "X-XSS-Protection", 874 | "description": "Cross-site scripting (XSS) filter" 875 | } 876 | ], 877 | "cookie": [], 878 | "body": "{\"id\":2,\"postTitle\":\"A goodsss day\",\"postSlug\":\"a-goodsss-day-1532319545369\",\"postType\":\"post\",\"postDate\":\"2018-07-02T00:36:26.371Z\",\"postContent\":\"Reprehenderit cum delectus voluptas. Aut ut veritatis. Maiores voluptas voluptas officia tenetur qui eligendi quam perspiciatis. Delectus maiores aut laboriosam quae sunt.\\n \\rVoluptas labore distinctio quisquam ratione esse. Eaque dolores aut cumque soluta consectetur sed nihil. Iusto qui quos a sit inventore vel cumque facere quibusdam. Mollitia nam facere natus.\\n \\rTempora esse eos quasi. Rerum cum omnis. Veniam laboriosam alias. Aliquid alias quisquam ratione pariatur placeat nobis labore. Debitis sed deserunt.\",\"postAuthor\":\"Brutus Maximus\",\"postImage\":\"/url\",\"postMedia\":\"/url\",\"postStatus\":\"published\",\"postExpiry\":\"2019-05-16T16:49:00.638Z\",\"postFrequency\":\"\",\"createdAt\":\"2018-07-23T04:17:39.574Z\",\"updatedAt\":\"2018-07-23T04:19:05.370Z\"}" 879 | } 880 | ] 881 | }, 882 | { 883 | "name": "localhost:1337/api/posts-delete", 884 | "request": { 885 | "method": "DELETE", 886 | "header": [ 887 | { 888 | "key": "Content-Type", 889 | "value": "application/json" 890 | }, 891 | { 892 | "key": "Authorization", 893 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI" 894 | } 895 | ], 896 | "body": { 897 | "mode": "raw", 898 | "raw": "" 899 | }, 900 | "url": { 901 | "raw": "localhost:1337/api/posts/a-new-post-1532319414478", 902 | "host": [ 903 | "localhost" 904 | ], 905 | "port": "1337", 906 | "path": [ 907 | "api", 908 | "posts", 909 | "a-new-post-1532319414478" 910 | ] 911 | } 912 | }, 913 | "response": [ 914 | { 915 | "id": "2209906a-504e-4d59-a197-b59995ab436d", 916 | "name": "localhost:1337/api/posts-delete", 917 | "originalRequest": { 918 | "method": "DELETE", 919 | "header": [ 920 | { 921 | "key": "Content-Type", 922 | "value": "application/json", 923 | "disabled": false 924 | }, 925 | { 926 | "key": "Authorization", 927 | "value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU2OTEzMGViLWVkZjItNDg1ZS1iMDNhLWZiZDlmYjhlZDNjZiIsInVzZXJOYW1lIjoiSm9obiBVc2VyIiwidXNlckVtYWlsIjoidXNlckBlbWFpbC5jb20iLCJ1c2VyUGFzcyI6IiQyYiQxMCR3RVNtMXNJb2ZrYmJsNnBrSG9XeW91VWpnaFhqekRRRU1sWTdxNXdVN0NITlU0ZXFLMlE3MiIsImNyZWF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsInVwZGF0ZWRBdCI6IjIwMTgtMDctMjNUMDI6Mzg6MDkuMzc0WiIsImlhdCI6MTUzMjMxMzUwMCwiZXhwIjoxNTM0OTA1NTAwfQ.SfEBe7MW46BnN2Gjo2pksb7cpnITs5UVnW5zlDCsajI", 928 | "disabled": false 929 | } 930 | ], 931 | "body": { 932 | "mode": "raw", 933 | "raw": "" 934 | }, 935 | "url": { 936 | "raw": "localhost:1337/api/posts/a-new-post-1532319414478", 937 | "host": [ 938 | "localhost" 939 | ], 940 | "port": "1337", 941 | "path": [ 942 | "api", 943 | "posts", 944 | "a-new-post-1532319414478" 945 | ] 946 | } 947 | }, 948 | "status": "OK", 949 | "code": 200, 950 | "_postman_previewlanguage": "json", 951 | "header": [ 952 | { 953 | "key": "Access-Control-Allow-Origin", 954 | "value": "*", 955 | "name": "Access-Control-Allow-Origin", 956 | "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." 957 | }, 958 | { 959 | "key": "Connection", 960 | "value": "keep-alive", 961 | "name": "Connection", 962 | "description": "Options that are desired for the connection" 963 | }, 964 | { 965 | "key": "Content-Length", 966 | "value": "40", 967 | "name": "Content-Length", 968 | "description": "The length of the response body in octets (8-bit bytes)" 969 | }, 970 | { 971 | "key": "Content-Type", 972 | "value": "application/json; charset=utf-8", 973 | "name": "Content-Type", 974 | "description": "The mime type of this content" 975 | }, 976 | { 977 | "key": "Date", 978 | "value": "Mon, 23 Jul 2018 04:20:24 GMT", 979 | "name": "Date", 980 | "description": "The date and time that the message was sent" 981 | }, 982 | { 983 | "key": "ETag", 984 | "value": "W/\"28-lhVHv+7L+msMz2SssxfwWOkXoP0\"", 985 | "name": "ETag", 986 | "description": "An identifier for a specific version of a resource, often a message digest" 987 | }, 988 | { 989 | "key": "Strict-Transport-Security", 990 | "value": "max-age=15552000; includeSubDomains", 991 | "name": "Strict-Transport-Security", 992 | "description": "A HSTS Policy informing the HTTP client how long to cache the HTTPS only policy and whether this applies to subdomains." 993 | }, 994 | { 995 | "key": "Vary", 996 | "value": "Accept-Encoding", 997 | "name": "Vary", 998 | "description": "Tells downstream proxies how to match future request headers to decide whether the cached response can be used rather than requesting a fresh one from the origin server." 999 | }, 1000 | { 1001 | "key": "X-Content-Type-Options", 1002 | "value": "nosniff", 1003 | "name": "X-Content-Type-Options", 1004 | "description": "The only defined value, \"nosniff\", prevents Internet Explorer from MIME-sniffing a response away from the declared content-type" 1005 | }, 1006 | { 1007 | "key": "X-DNS-Prefetch-Control", 1008 | "value": "off", 1009 | "name": "X-DNS-Prefetch-Control", 1010 | "description": "Custom header" 1011 | }, 1012 | { 1013 | "key": "X-Download-Options", 1014 | "value": "noopen", 1015 | "name": "X-Download-Options", 1016 | "description": "Custom header" 1017 | }, 1018 | { 1019 | "key": "X-Frame-Options", 1020 | "value": "SAMEORIGIN", 1021 | "name": "X-Frame-Options", 1022 | "description": "Clickjacking protection: \"deny\" - no rendering within a frame, \"sameorigin\" - no rendering if origin mismatch" 1023 | }, 1024 | { 1025 | "key": "X-XSS-Protection", 1026 | "value": "1; mode=block", 1027 | "name": "X-XSS-Protection", 1028 | "description": "Cross-site scripting (XSS) filter" 1029 | } 1030 | ], 1031 | "cookie": [], 1032 | "body": "{\"success\":\"Post successfully deleted.\"}" 1033 | } 1034 | ] 1035 | } 1036 | ] 1037 | } 1038 | ] 1039 | } -------------------------------------------------------------------------------- /test/user/user.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const app = require('../../server/server'); 4 | const request = require('supertest'); 5 | const expect = require('chai').expect; 6 | 7 | describe('[USER] /api/users Testing', () => { 8 | it('should be able to get user credentials', (done) => { 9 | request(app) 10 | .post('/api/users') 11 | .expect(201) 12 | .end((err, res) => { 13 | expect(res.body).to.be.an('object'); 14 | done(); 15 | }); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/utils/helpers.test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const helpers = require('../../server/utils/helpers'); 3 | 4 | describe('[Helpers] Unit Test - validateStringLength', () => { 5 | it('returns give error if input length > limit', () => { 6 | const error = helpers.validateStringLength('Hello World', 5); 7 | expect(error).to.equal('* Cannot be more than 5 characters'); 8 | }); 9 | 10 | it('returns error if input is empty', () => { 11 | const error = helpers.validateStringLength('', 5); 12 | expect(error).to.equal('* Cannot be empty'); 13 | }); 14 | 15 | it('returns empty string if there is no error', () => { 16 | const error = helpers.validateStringLength('Hello World!', 20); 17 | expect(error).to.equal(''); 18 | }); 19 | }); 20 | 21 | describe('[Helpers] Unit Test - validatePassword', () => { 22 | it('returns error if length > 50 chars', () => { 23 | const password = 'Aas9df8sd9!@#sjsdlkfjdskfdsjfkldslkfjldkfjlkdsjflk dsjfkljdsklfjdsklfjlkdsfdsfddsdfsi4y34y0fhelkjvfsjf934otkshdlf9s8f0sdfsdfsdfuds0u230r9uwefdfssfdsfdsfsdf'; 24 | const error = helpers.validatePassword(password); 25 | expect(error[0]).to.equal('* Must be fewer than 50 chars'); 26 | }); 27 | 28 | it('returns error if length < 8 chars', () => { 29 | const error = helpers.validatePassword('fd4%!D'); 30 | expect(error[0]).to.equal('* Must be longer than 7 chars'); 31 | }); 32 | 33 | }); 34 | 35 | describe('[Helpers] Unit Test - validateEmail', () => { 36 | it('returns error if it is longer than 40 chars', () => { 37 | const error = helpers.validateEmail('Asdfdfdsf4234234324324dsfjsdkflsdkfsdfskdfdsklfsd23434324234fdsfdsfsdf@cc.cc'); 38 | expect(error).to.equal('* Email is too long, please use shorter email address'); 39 | }); 40 | 41 | it('returns no error if it is in correct foramt', () => { 42 | const error = helpers.validateEmail('fsdf.c.c'); 43 | expect(error).to.equal('* Email must be in valid format'); 44 | }); 45 | }); 46 | --------------------------------------------------------------------------------