├── config ├── test.yml ├── production.yml ├── development.yml ├── default.yml └── custom-environment-variables.yml ├── Procfile ├── logo.png ├── src ├── lib │ ├── bootstrap.js │ ├── env.js │ ├── db.js │ ├── server.js │ ├── utils.js │ ├── logger.js │ ├── relations-map.js │ ├── app.js │ └── errors.js ├── routes │ ├── tags-router.js │ ├── users-router.js │ ├── index.js │ ├── profiles-router.js │ └── articles-router.js ├── controllers │ ├── tags-controller.js │ ├── index.js │ ├── comments-controller.js │ ├── profiles-controller.js │ ├── users-controller.js │ └── articles-controller.js ├── middleware │ ├── auth-required-middleware.js │ ├── camelize-middleware.js │ ├── user-middleware.js │ ├── jwt-middleware.js │ ├── pager-middleware.js │ └── error-middleware.js ├── schemas │ ├── index.js │ ├── tag-schema.js │ ├── comment-schema.js │ ├── user-schema.js │ ├── article-schema.js │ └── time-stamp-schema.js ├── seeds │ ├── 03-tags.js │ ├── 01-users.js │ └── 02-articles.js ├── bin │ └── server.js └── migrations │ └── 20170422213822_init.js ├── .example-env ├── Dockerfile ├── docker-compose.yml ├── .gitignore ├── run-api-tests.sh ├── knexfile.js ├── readme.md ├── package.json └── Conduit.postman_collection.json /config/test.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/production.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start 2 | -------------------------------------------------------------------------------- /config/development.yml: -------------------------------------------------------------------------------- 1 | env: 2 | logLevel: debug 3 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SameerBadriddinov/backend-application/HEAD/logo.png -------------------------------------------------------------------------------- /src/lib/bootstrap.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config() 2 | require("./env") 3 | require("config") 4 | -------------------------------------------------------------------------------- /src/lib/env.js: -------------------------------------------------------------------------------- 1 | if (!process.env.NODE_ENV) { 2 | process.env.NODE_ENV = "development" 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/db.js: -------------------------------------------------------------------------------- 1 | const knex = require("knex") 2 | const knexfile = require("../../knexfile") 3 | const db = knex(knexfile) 4 | 5 | module.exports = db 6 | -------------------------------------------------------------------------------- /.example-env: -------------------------------------------------------------------------------- 1 | NODE_ENV = development 2 | PORT = 3000 3 | SECRET = secret 4 | 5 | #DB_CLIENT = sqlite3 | pg 6 | #DB_CONNECTION = postgres://user:password@localhost:5432/db_name 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.18.3 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run db:migrate 12 | 13 | RUN npm run db:load 14 | -------------------------------------------------------------------------------- /src/routes/tags-router.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const ctrl = require("../controllers").tags 3 | const router = new Router() 4 | 5 | router.get("/tags", ctrl.get) 6 | 7 | module.exports = router.routes() 8 | -------------------------------------------------------------------------------- /src/controllers/tags-controller.js: -------------------------------------------------------------------------------- 1 | const db = require("../lib/db") 2 | 3 | module.exports = { 4 | async get(ctx) { 5 | const tags = await db("tags").pluck("name") 6 | 7 | ctx.body = { tags } 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /src/middleware/auth-required-middleware.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationError } = require("../lib/errors") 2 | 3 | module.exports = function(ctx, next) { 4 | ctx.assert(ctx.state.user, new AuthenticationError()) 5 | return next() 6 | } 7 | -------------------------------------------------------------------------------- /config/default.yml: -------------------------------------------------------------------------------- 1 | server: 2 | host: 0.0.0.0 3 | port: 3000 4 | domain: example.com 5 | 6 | env: 7 | environment: local 8 | logLevel: info 9 | name: app 10 | version: 0.0.0 11 | 12 | secret: secret 13 | 14 | db: 15 | client: sqlite3 16 | -------------------------------------------------------------------------------- /src/middleware/camelize-middleware.js: -------------------------------------------------------------------------------- 1 | const humps = require("humps") 2 | const _ = require("lodash") 3 | 4 | module.exports = async function(ctx, next) { 5 | await next() 6 | if (ctx.body && _.isObjectLike(ctx.body)) { 7 | ctx.body = humps.camelizeKeys(ctx.body) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/controllers/index.js: -------------------------------------------------------------------------------- 1 | const users = require("./users-controller") 2 | const tags = require("./tags-controller") 3 | const profiles = require("./profiles-controller") 4 | const articles = require("./articles-controller") 5 | 6 | module.exports = { 7 | users, 8 | tags, 9 | profiles, 10 | articles, 11 | } 12 | -------------------------------------------------------------------------------- /config/custom-environment-variables.yml: -------------------------------------------------------------------------------- 1 | server: 2 | host: HOST 3 | port: PORT 4 | domain: DOMAIN 5 | 6 | env: 7 | environment: ENVIRONMENT 8 | logLevel: LOG_LEVEL 9 | name: npm_package_name 10 | version: npm_package_version 11 | 12 | secret: SECRET 13 | 14 | db: 15 | client: DB_CLIENT 16 | connection: DB_CONNECTION 17 | -------------------------------------------------------------------------------- /src/schemas/index.js: -------------------------------------------------------------------------------- 1 | const user = require("./user-schema") 2 | const article = require("./article-schema") 3 | const comment = require("./comment-schema") 4 | const tag = require("./tag-schema") 5 | 6 | module.exports = function(app) { 7 | app.schemas = { 8 | user, 9 | article, 10 | comment, 11 | tag, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/seeds/03-tags.js: -------------------------------------------------------------------------------- 1 | const uuid = require("uuid") 2 | 3 | const tags = [ 4 | { 5 | id: uuid(), 6 | name: "dragons", 7 | }, 8 | { 9 | id: uuid(), 10 | name: "coffee", 11 | }, 12 | ] 13 | 14 | exports.seed = async function(knex) { 15 | await knex("tags").del() 16 | 17 | return knex("tags").insert(tags) 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | api: 5 | build: 6 | context: . 7 | container_name: koa-knex-realworld-example-api 8 | command: npm run start 9 | restart: unless-stopped 10 | ports: 11 | - "3000:3000" 12 | volumes: 13 | - db:/usr/src/app/data 14 | 15 | volumes: 16 | db: 17 | 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /bower_components 6 | 7 | # IDEs and editors 8 | /.idea 9 | .project 10 | .classpath 11 | *.launch 12 | .settings/ 13 | 14 | 15 | # System Files 16 | .DS_Store 17 | Thumbs.db 18 | 19 | # logs 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # tmp 25 | data 26 | .env 27 | -------------------------------------------------------------------------------- /src/routes/users-router.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const ctrl = require("../controllers").users 3 | const router = new Router() 4 | 5 | const auth = require("../middleware/auth-required-middleware") 6 | 7 | router.post("/users/login", ctrl.login) 8 | router.post("/users", ctrl.post) 9 | 10 | router.get("/user", auth, ctrl.get) 11 | router.put("/user", auth, ctrl.put) 12 | 13 | module.exports = router.routes() 14 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const router = new Router() 3 | const api = new Router() 4 | 5 | const users = require("./users-router") 6 | const articles = require("./articles-router") 7 | const profiles = require("./profiles-router") 8 | const tags = require("./tags-router") 9 | 10 | api.use(users) 11 | api.use(articles) 12 | api.use(profiles) 13 | api.use(tags) 14 | 15 | router.use("/api", api.routes()) 16 | 17 | module.exports = router 18 | -------------------------------------------------------------------------------- /src/lib/server.js: -------------------------------------------------------------------------------- 1 | const http = require("http") 2 | const stoppable = require("stoppable") 3 | const pEvent = require("p-event") 4 | const util = require("util") 5 | 6 | module.exports = async function createServerAndListen(app, port, host) { 7 | const server = stoppable(http.createServer(app.callback()), 7000) 8 | 9 | server.listen(port, host) 10 | 11 | server.stop = util.promisify(server.stop) 12 | 13 | await pEvent(server, "listening") 14 | 15 | return server 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/profiles-router.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const ctrl = require("../controllers").profiles 3 | const router = new Router() 4 | 5 | const auth = require("../middleware/auth-required-middleware") 6 | 7 | router.param("username", ctrl.byUsername) 8 | 9 | router.get("/profiles/:username", ctrl.get) 10 | router.post("/profiles/:username/follow", auth, ctrl.follow.post) 11 | router.del("/profiles/:username/follow", auth, ctrl.follow.del) 12 | 13 | module.exports = router.routes() 14 | -------------------------------------------------------------------------------- /src/middleware/user-middleware.js: -------------------------------------------------------------------------------- 1 | const { has } = require("lodash") 2 | const db = require("../lib/db") 3 | 4 | module.exports = async (ctx, next) => { 5 | if (has(ctx, "state.jwt.sub.id")) { 6 | ctx.state.user = await db("users") 7 | .first( 8 | "id", 9 | "email", 10 | "username", 11 | "image", 12 | "bio", 13 | "created_at", 14 | "updated_at", 15 | ) 16 | .where({ id: ctx.state.jwt.sub.id }) 17 | } 18 | 19 | return next() 20 | } 21 | -------------------------------------------------------------------------------- /run-api-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -x 3 | 4 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" 5 | 6 | APIURL=${APIURL:-https://conduit.productionready.io/api} 7 | USERNAME=${USERNAME:-u`date +%s`} 8 | EMAIL=${EMAIL:-$USERNAME@mail.com} 9 | PASSWORD=${PASSWORD:-password} 10 | 11 | npx newman run $SCRIPTDIR/Conduit.postman_collection.json \ 12 | --delay-request 500 \ 13 | --global-var "APIURL=$APIURL" \ 14 | --global-var "USERNAME=$USERNAME" \ 15 | --global-var "EMAIL=$EMAIL" \ 16 | --global-var "PASSWORD=$PASSWORD" -------------------------------------------------------------------------------- /src/middleware/jwt-middleware.js: -------------------------------------------------------------------------------- 1 | const koaJwt = require("koa-jwt") 2 | const config = require("config") 3 | 4 | module.exports = koaJwt({ 5 | getToken(ctx, opts) { 6 | const { authorization } = ctx.header 7 | 8 | if (authorization && authorization.split(" ")[0] === "Bearer") { 9 | return authorization.split(" ")[1] 10 | } 11 | 12 | if (authorization && authorization.split(" ")[0] === "Token") { 13 | return authorization.split(" ")[1] 14 | } 15 | 16 | return null 17 | }, 18 | secret: config.get("secret"), 19 | passthrough: true, 20 | key: "jwt", 21 | }) 22 | -------------------------------------------------------------------------------- /src/schemas/tag-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require("yup") 2 | const timeStampSchema = require("./time-stamp-schema") 3 | const isUUID = require("validator/lib/isUUID") 4 | 5 | const tagSchema = yup 6 | .object() 7 | .shape({ 8 | id: yup.string().test({ 9 | name: "id", 10 | message: "${path} must be uuid", // eslint-disable-line 11 | test: value => (value ? isUUID(value) : true), 12 | }), 13 | 14 | name: yup 15 | .string() 16 | .required() 17 | .max(30) 18 | .trim(), 19 | }) 20 | .noUnknown() 21 | .concat(timeStampSchema) 22 | 23 | module.exports = tagSchema 24 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | const config = require("config") 2 | const jwt = require("jsonwebtoken") 3 | const _ = require("lodash") 4 | 5 | function generateJWTforUser(user = {}) { 6 | return Object.assign({}, user, { 7 | token: jwt.sign( 8 | { 9 | sub: _.pick(user, ["id", "email", "username"]), 10 | }, 11 | config.get("secret"), 12 | { 13 | expiresIn: "7d", 14 | }, 15 | ), 16 | }) 17 | } 18 | 19 | function getSelect(table, prefix, fields) { 20 | return fields.map(f => `${table}.${f} as ${prefix}_${f}`) 21 | } 22 | 23 | exports.generateJWTforUser = generateJWTforUser 24 | exports.getSelect = getSelect 25 | -------------------------------------------------------------------------------- /src/middleware/pager-middleware.js: -------------------------------------------------------------------------------- 1 | const qs = require("qs") 2 | 3 | const filters = ["tag", "author", "favorited"] 4 | 5 | module.exports = (ctx, next) => { 6 | if (ctx.method !== "GET") { 7 | return next() 8 | } 9 | 10 | ctx.query = qs.parse(ctx.querystring) 11 | 12 | const { query } = ctx 13 | 14 | query.limit = parseInt(query.limit, 10) || 20 15 | query.skip = query.offset = parseInt(query.offset, 10) || 0 16 | 17 | if (query.page) { 18 | query.page = parseInt(query.page, 10) 19 | query.skip = query.offset = (query.page - 1) * query.limit 20 | } 21 | 22 | filters.forEach(f => { 23 | if (!query[f] || Array.isArray(query[f])) return 24 | if (query[f]) { 25 | query[f] = [query[f]] 26 | } 27 | }) 28 | 29 | return next() 30 | } 31 | -------------------------------------------------------------------------------- /src/routes/articles-router.js: -------------------------------------------------------------------------------- 1 | const Router = require("koa-router") 2 | const ctrl = require("../controllers").articles 3 | const router = new Router() 4 | 5 | const auth = require("../middleware/auth-required-middleware") 6 | 7 | router.param("slug", ctrl.bySlug) 8 | router.param("comment", ctrl.comments.byComment) 9 | 10 | router.get("/articles", ctrl.get) 11 | router.post("/articles", auth, ctrl.post) 12 | 13 | router.get("/articles/feed", auth, ctrl.feed.get) 14 | 15 | router.get("/articles/:slug", ctrl.getOne) 16 | router.put("/articles/:slug", auth, ctrl.put) 17 | router.del("/articles/:slug", auth, ctrl.del) 18 | 19 | router.post("/articles/:slug/favorite", auth, ctrl.favorite.post) 20 | router.del("/articles/:slug/favorite", auth, ctrl.favorite.del) 21 | 22 | router.get("/articles/:slug/comments", ctrl.comments.get) 23 | router.post("/articles/:slug/comments", auth, ctrl.comments.post) 24 | router.del("/articles/:slug/comments/:comment", auth, ctrl.comments.del) 25 | 26 | module.exports = router.routes() 27 | -------------------------------------------------------------------------------- /src/schemas/comment-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require("yup") 2 | const timeStampSchema = require("./time-stamp-schema") 3 | const isUUID = require("validator/lib/isUUID") 4 | 5 | const commentSchema = yup 6 | .object() 7 | .shape({ 8 | id: yup.string().test({ 9 | name: "id", 10 | message: "${path} must be uuid", // eslint-disable-line 11 | test: value => (value ? isUUID(value) : true), 12 | }), 13 | 14 | author: yup.string().test({ 15 | name: "user", 16 | message: "${path} must be uuid", // eslint-disable-line 17 | test: value => (value ? isUUID(value) : true), 18 | }), 19 | 20 | article: yup.string().test({ 21 | name: "article", 22 | message: "${path} must be uuid", // eslint-disable-line 23 | test: value => (value ? isUUID(value) : true), 24 | }), 25 | 26 | body: yup 27 | .string() 28 | .required() 29 | .trim(), 30 | }) 31 | .noUnknown() 32 | .concat(timeStampSchema) 33 | 34 | module.exports = commentSchema 35 | -------------------------------------------------------------------------------- /src/lib/logger.js: -------------------------------------------------------------------------------- 1 | const config = require("config") 2 | const pino = require("pino") 3 | const colada = require("pino-colada") 4 | const os = require("os") 5 | const miss = require("mississippi") 6 | 7 | const serializers = { 8 | req: req => { 9 | return pino.stdSerializers.req(req) 10 | }, 11 | res: pino.stdSerializers.res, 12 | err: pino.stdSerializers.err, 13 | error: pino.stdSerializers.err, 14 | user: user => ({ 15 | id: user._id, 16 | }), 17 | } 18 | 19 | const opts = { 20 | level: config.get("env.logLevel"), 21 | serializers, 22 | base: { 23 | NODE_ENV: process.env.NODE_ENV, 24 | environment: config.get("env.environment"), 25 | version: config.get("env.version"), 26 | name: config.get("env.name"), 27 | pid: process.pid, 28 | hostname: os.hostname(), 29 | }, 30 | } 31 | 32 | const stream = 33 | process.env.NODE_ENV === "production" 34 | ? pino.destination(1) 35 | : miss.pipeline(colada(), pino.destination(1)) 36 | 37 | const logger = pino(opts, stream) 38 | 39 | module.exports = logger 40 | module.exports.serializers = serializers 41 | -------------------------------------------------------------------------------- /src/schemas/user-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require("yup") 2 | const timeStampSchema = require("./time-stamp-schema") 3 | const isUUID = require("validator/lib/isUUID") 4 | 5 | const userSchema = yup 6 | .object() 7 | .shape({ 8 | id: yup.string().test({ 9 | name: "id", 10 | message: "${path} must be uuid", // eslint-disable-line 11 | test: value => (value ? isUUID(value) : true), 12 | }), 13 | 14 | email: yup 15 | .string() 16 | .required() 17 | .email() 18 | .lowercase() 19 | .trim(), 20 | 21 | password: yup.string().when("$validatePassword", { 22 | is: true, 23 | then: yup 24 | .string() 25 | .required() 26 | .min(8) 27 | .max(30), 28 | }), 29 | 30 | username: yup 31 | .string() 32 | .required() 33 | .max(30) 34 | .default("") 35 | .trim(), 36 | 37 | image: yup 38 | .string() 39 | .url() 40 | .default("") 41 | .trim(), 42 | 43 | bio: yup 44 | .string() 45 | .default("") 46 | .trim(), 47 | }) 48 | .noUnknown() 49 | .concat(timeStampSchema) 50 | 51 | module.exports = userSchema 52 | -------------------------------------------------------------------------------- /src/schemas/article-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require("yup") 2 | const timeStampSchema = require("./time-stamp-schema") 3 | const isUUID = require("validator/lib/isUUID") 4 | 5 | const articleSchema = yup 6 | .object() 7 | .shape({ 8 | id: yup.string().test({ 9 | name: "id", 10 | message: "${path} must be uuid", // eslint-disable-line 11 | test: value => (value ? isUUID(value) : true), 12 | }), 13 | 14 | author: yup.string().test({ 15 | name: "user", 16 | message: "${path} must be uuid", // eslint-disable-line 17 | test: value => (value ? isUUID(value) : true), 18 | }), 19 | 20 | slug: yup.string().trim(), 21 | 22 | title: yup 23 | .string() 24 | .required() 25 | .trim(), 26 | 27 | body: yup 28 | .string() 29 | .required() 30 | .trim(), 31 | 32 | description: yup 33 | .string() 34 | .required() 35 | .trim(), 36 | 37 | favoritesCount: yup 38 | .number() 39 | .required() 40 | .default(0), 41 | 42 | tagList: yup.array().of(yup.string()), 43 | }) 44 | .noUnknown() 45 | .concat(timeStampSchema) 46 | 47 | module.exports = articleSchema 48 | -------------------------------------------------------------------------------- /src/lib/relations-map.js: -------------------------------------------------------------------------------- 1 | const userFields = ["id", "image", "bio", "username"] 2 | 3 | const articleFields = [ 4 | "id", 5 | "slug", 6 | "title", 7 | "body", 8 | "description", 9 | "favorites_count", 10 | "created_at", 11 | "updated_at", 12 | ] 13 | 14 | const commentFields = ["id", "body", "created_at", "updated_at"] 15 | 16 | const relationsMaps = [ 17 | { 18 | mapId: "articleMap", 19 | idProperty: "id", 20 | properties: [...articleFields, "favorited"], 21 | associations: [ 22 | { name: "author", mapId: "userMap", columnPrefix: "author_" }, 23 | ], 24 | collections: [{ name: "tagList", mapId: "tagMap", columnPrefix: "tag_" }], 25 | }, 26 | { 27 | mapId: "commentMap", 28 | idProperty: "id", 29 | properties: [...commentFields], 30 | associations: [ 31 | { name: "author", mapId: "userMap", columnPrefix: "author_" }, 32 | ], 33 | }, 34 | { 35 | mapId: "userMap", 36 | idProperty: "id", 37 | properties: [...userFields, "following"], 38 | }, 39 | { 40 | mapId: "tagMap", 41 | idProperty: "id", 42 | properties: ["id", "name"], 43 | }, 44 | ] 45 | 46 | exports.relationsMaps = relationsMaps 47 | exports.userFields = userFields 48 | exports.articleFields = articleFields 49 | exports.commentFields = commentFields 50 | -------------------------------------------------------------------------------- /src/schemas/time-stamp-schema.js: -------------------------------------------------------------------------------- 1 | const yup = require("yup") 2 | const isISO8601 = require("validator/lib/isISO8601") 3 | 4 | const timeStampsSchema = yup 5 | .object() 6 | .shape({ 7 | createdAt: yup 8 | .string() 9 | .required() 10 | .test({ 11 | name: "createdAt", 12 | message: "${path} must be valid ISO8601 date", // eslint-disable-line 13 | test: value => 14 | value ? isISO8601(new Date(value).toISOString()) : true, 15 | }) 16 | .transform(function(value) { 17 | return this.isType(value) && value !== null 18 | ? new Date(value).toISOString() 19 | : value 20 | }) 21 | .default(() => new Date().toISOString()), 22 | 23 | updatedAt: yup 24 | .string() 25 | .required() 26 | .test({ 27 | name: "updatedAt", 28 | message: "${path} must be valid ISO8601 date", // eslint-disable-line 29 | test: value => 30 | value ? isISO8601(new Date(value).toISOString()) : true, 31 | }) 32 | .transform(function(value) { 33 | return this.isType(value) && value !== null 34 | ? new Date(value).toISOString() 35 | : value 36 | }) 37 | .default(() => new Date().toISOString()), 38 | }) 39 | .noUnknown() 40 | 41 | module.exports = timeStampsSchema 42 | -------------------------------------------------------------------------------- /src/bin/server.js: -------------------------------------------------------------------------------- 1 | require("../lib/bootstrap") 2 | const pEvent = require("p-event") 3 | const createServerAndListen = require("../lib/server") 4 | const config = require("config") 5 | const logger = require("../lib/logger") 6 | const db = require("../lib/db") 7 | 8 | const app = require("../lib/app") 9 | 10 | async function main() { 11 | const host = config.get("server.host") 12 | const port = config.get("server.port") 13 | let server 14 | 15 | try { 16 | await db.select(db.raw("1")) 17 | logger.debug("Database connected") 18 | 19 | server = await createServerAndListen(app, port, host) 20 | logger.debug(`Server is listening on: ${host}:${port}`) 21 | 22 | await Promise.race([ 23 | ...["SIGINT", "SIGHUP", "SIGTERM"].map(s => 24 | pEvent(process, s, { 25 | rejectionEvents: ["uncaughtException", "unhandledRejection"], 26 | }), 27 | ), 28 | ]) 29 | } catch (err) { 30 | process.exitCode = 1 31 | logger.fatal(err) 32 | } finally { 33 | if (server) { 34 | logger.debug("Close server") 35 | await server.stop() 36 | logger.debug("Server closed") 37 | } 38 | 39 | logger.debug("Close database") 40 | await db.destroy() 41 | logger.debug("Database closed") 42 | 43 | setTimeout(() => process.exit(), 10000).unref() 44 | } 45 | } 46 | 47 | main() 48 | -------------------------------------------------------------------------------- /src/seeds/01-users.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require("bcryptjs") 2 | const faker = require("faker") 3 | 4 | const users = [ 5 | { 6 | name: "admin", 7 | id: "345ae4d0-f2c3-4342-91a2-5b45cb8db57f", 8 | }, 9 | { 10 | name: "demo", 11 | id: "16c1ef84-df72-4be1-ad46-1168ee53cd60", 12 | }, 13 | { 14 | name: "jack", 15 | id: "b8d2586f-4746-418c-82b2-db9eff7a7f42", 16 | }, 17 | { 18 | name: "johnjacob", 19 | email: "john@jacob.com", 20 | id: "52e1cc10-20b9-4cf2-ad94-3b0c135d35a5", 21 | }, 22 | ] 23 | 24 | function getUsers() { 25 | return users.map(u => { 26 | return { 27 | id: u.id, 28 | email: u.email || `${u.name}@demo.com`, 29 | username: u.name, 30 | password: bcrypt.hashSync("X12345678", 10), 31 | bio: faker.lorem.sentences(), 32 | image: faker.image.avatar(), 33 | created_at: new Date().toISOString(), 34 | updated_at: new Date().toISOString(), 35 | } 36 | }) 37 | } 38 | 39 | exports.getUsers = getUsers 40 | 41 | exports.seed = async function(knex) { 42 | if (process.env.NODE_ENV === "production") { 43 | await knex("users") 44 | .whereIn( 45 | "email", 46 | users.map(u => u.email || `${u.name}@demo.com`), 47 | ) 48 | .del() 49 | } else { 50 | await knex("users").del() 51 | } 52 | 53 | return knex("users").insert(getUsers()) 54 | } 55 | -------------------------------------------------------------------------------- /src/seeds/02-articles.js: -------------------------------------------------------------------------------- 1 | const faker = require("faker") 2 | const _ = require("lodash") 3 | const uuid = require("uuid") 4 | const slug = require("slug") 5 | const { subMonths } = require("date-fns") 6 | const { getUsers } = require("./01-users") 7 | 8 | function getArticles(users) { 9 | return _.flatMap(users, function(user) { 10 | return Array.from({ length: 5 }, function() { 11 | const title = faker.lorem.sentence() 12 | const date = faker.date 13 | .between(subMonths(new Date(), 18), new Date()) 14 | .toISOString() 15 | 16 | return { 17 | id: uuid(), 18 | author: user.id, 19 | title, 20 | slug: slug(title, { lower: true }), 21 | body: faker.lorem.sentences(10), 22 | description: faker.lorem.sentences(2), 23 | created_at: date, 24 | updated_at: date, 25 | } 26 | }) 27 | }) 28 | } 29 | 30 | exports.getArticles = getArticles 31 | 32 | exports.seed = async function(knex) { 33 | const users = getUsers() 34 | 35 | if (process.env.NODE_ENV === "production") { 36 | await knex("articles") 37 | .whereIn( 38 | "author", 39 | users.map(u => u.id), 40 | ) 41 | .del() 42 | } else { 43 | await knex("articles").del() 44 | } 45 | 46 | const articles = getArticles(users) 47 | 48 | return knex("articles").insert(articles) 49 | } 50 | -------------------------------------------------------------------------------- /knexfile.js: -------------------------------------------------------------------------------- 1 | require("./src/lib/bootstrap") 2 | const config = require("config") 3 | const fs = require("fs") 4 | 5 | if (config.get("db.client") === "sqlite3") { 6 | try { 7 | fs.mkdirSync("data") 8 | } catch (err) { 9 | if (err.code !== "EEXIST") { 10 | throw err 11 | } 12 | } 13 | } 14 | 15 | const dbClient = config.get("db.client") 16 | const dbConnection = config.has("db.connection") && config.get("db.connection") 17 | 18 | const options = { 19 | client: dbClient, 20 | connection: dbConnection || { 21 | filename: "data/dev.sqlite3", 22 | }, 23 | migrations: { 24 | directory: "src/migrations", 25 | tableName: "migrations", 26 | }, 27 | debug: false, 28 | seeds: { 29 | directory: "src/seeds", 30 | }, 31 | useNullAsDefault: dbClient === "sqlite3", 32 | } 33 | 34 | if (dbClient !== "sqlite3") { 35 | options.pool = { 36 | min: 2, 37 | max: 10, 38 | } 39 | } 40 | 41 | const configs = { 42 | development: Object.assign({}, options), 43 | 44 | test: Object.assign({}, options, { 45 | connection: dbConnection || { 46 | filename: "data/test.sqlite3", 47 | }, 48 | }), 49 | 50 | production: Object.assign({}, options, { 51 | connection: dbConnection || { 52 | filename: "data/prod.sqlite3", 53 | }, 54 | }), 55 | } 56 | 57 | Object.assign(configs, configs[process.env.NODE_ENV]) 58 | 59 | module.exports = configs 60 | -------------------------------------------------------------------------------- /src/lib/app.js: -------------------------------------------------------------------------------- 1 | const config = require("config") 2 | const Koa = require("koa") 3 | 4 | const app = new Koa() 5 | 6 | app.proxy = true 7 | 8 | app.keys = [config.get("secret")] 9 | 10 | require("../schemas")(app) 11 | 12 | const responseTime = require("koa-response-time") 13 | const helmet = require("koa-helmet") 14 | const logger = require("koa-logger") 15 | const xRequestId = require("koa-x-request-id") 16 | const camelizeMiddleware = require("../middleware/camelize-middleware") 17 | const error = require("../middleware/error-middleware") 18 | const cors = require("kcors") 19 | const jwt = require("../middleware/jwt-middleware") 20 | const bodyParser = require("koa-bodyparser") 21 | const pagerMiddleware = require("../middleware/pager-middleware") 22 | const userMiddleware = require("../middleware/user-middleware") 23 | const routes = require("../routes") 24 | 25 | app.use(responseTime()) 26 | app.use(xRequestId({ inject: true }, app)) 27 | app.use(logger()) 28 | app.use(helmet()) 29 | app.use( 30 | cors({ 31 | origin: "*", 32 | exposeHeaders: ["Authorization"], 33 | credentials: true, 34 | allowMethods: ["GET", "PUT", "POST", "DELETE"], 35 | allowHeaders: ["Authorization", "Content-Type"], 36 | keepHeadersOnError: true, 37 | }), 38 | ) 39 | 40 | app.use(camelizeMiddleware) 41 | 42 | app.use(error) 43 | app.use(jwt) 44 | app.use( 45 | bodyParser({ 46 | enableTypes: ["json"], 47 | }), 48 | ) 49 | 50 | app.use(userMiddleware) 51 | app.use(pagerMiddleware) 52 | 53 | app.use(routes.routes()) 54 | app.use(routes.allowedMethods()) 55 | 56 | module.exports = app 57 | -------------------------------------------------------------------------------- /src/lib/errors.js: -------------------------------------------------------------------------------- 1 | const { ValidationError } = require("yup") 2 | const http = require("http") 3 | 4 | class AuthenticationError extends Error { 5 | constructor(message = http.STATUS_CODES[401]) { 6 | super(message) 7 | this.message = message 8 | this.statusCode = 401 9 | 10 | this.name = this.constructor.name 11 | Error.captureStackTrace(this, this.constructor) 12 | } 13 | } 14 | 15 | class AuthorizationError extends Error { 16 | constructor(message = http.STATUS_CODES[403]) { 17 | super(message) 18 | this.message = message 19 | this.statusCode = 403 20 | 21 | this.name = this.constructor.name 22 | Error.captureStackTrace(this, this.constructor) 23 | } 24 | } 25 | 26 | class NotFoundError extends Error { 27 | constructor(message = http.STATUS_CODES[404]) { 28 | super(message) 29 | this.message = message 30 | this.statusCode = 404 31 | 32 | this.name = this.constructor.name 33 | Error.captureStackTrace(this, this.constructor) 34 | } 35 | } 36 | 37 | class ServerError extends Error { 38 | constructor(message = http.STATUS_CODES[500]) { 39 | super(message) 40 | this.message = message 41 | this.statusCode = 500 42 | 43 | this.name = this.constructor.name 44 | Error.captureStackTrace(this, this.constructor) 45 | } 46 | } 47 | 48 | module.exports = { 49 | AuthenticationError, 50 | AuthorizationError, 51 | NotFoundError, 52 | ValidationError, 53 | ServerError, 54 | } 55 | 56 | // module.exports = { 57 | // UnauthorizedError, // 401 58 | // ForbiddenError, // 403 59 | // NotFoundError, // 404 60 | // ValidationError, // 422 61 | // ServerError, // 500 62 | // } 63 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![RealWorld Example App](logo.png) 2 | 3 | > ### Example Node.Js (Koa.js + Knex) codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the [RealWorld](https://github.com/gothinkster/realworld-example-apps) spec and API. 4 | 5 | This repo is functionality complete — PRs and issues welcome! 6 | 7 | This codebase was created to demonstrate a fully fledged fullstack application built with **Koa.js + Knex** including CRUD operations, authentication, routing, pagination, and more. 8 | 9 | We've gone to great lengths to adhere to the **Koa.js + Knex** community styleguides & best practices. 10 | 11 | For more information on how to this works with other frontends/backends, head over to the [RealWorld](https://github.com/gothinkster/realworld) repo. 12 | 13 | # Getting started 14 | 15 | ## Installation 16 | 17 | 1. Instal [Node.JS](https://nodejs.org/en/download/package-manager/) latest version 18 | 2. Clone this repo 19 | 3. Install dependencies, just run in project folder: `npm i` or `yarn` 20 | 21 | ## Usage 22 | 23 | 1. run `npm start` to start server 24 | 25 | ## Testing 26 | 27 | 1. run `npm test` for tests 28 | 29 | ## Server Configuration (optional) 30 | 31 | You can use `.env` file, to configure project like this: 32 | 33 | ``` 34 | NODE_ENV = development 35 | PORT = 3000 36 | SECRET = secret 37 | DB_CLIENT = sqlite3 38 | #DB_CONNECTION = postgres://user:password@localhost:5432/db_name 39 | ``` 40 | 41 | you can just copy `.example-env` 42 | 43 | ## Variables description 44 | 45 | `NODE_ENV` - specify env: development/production/test. `development` by default 46 | 47 | `NODE_PORT` - specify port: `3000` by default 48 | 49 | `SECRET` - custom secret for generating passwords. `secret` by default 50 | 51 | `DB_CLIENT` - database to use. `pg` - postgress or `sqlite3`. `sqlite3` by default 52 | 53 | `DB_CONNECTION` - db connection string for `postgress` database. 54 | 55 | ## Fixtures (optional) 56 | 57 | 1. load fixtures: `npm run db:load` (it uses settings from `.env`). Don't forget to set `NODE_ENV`. 58 | 59 | ## Styleguide 60 | 61 | [![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) 62 | 63 | # How it works 64 | 65 | > Describe the general architecture of your app here 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-knex-realworld-example", 3 | "version": "1.0.0", 4 | "description": "conduit on koa with knex", 5 | "main": "src/bin/server", 6 | "repository": "git@github.com:gothinkster/koa-knex-realworld-example.git", 7 | "author": "Dmitrii Solovev ", 8 | "license": "ISC", 9 | "scripts": { 10 | "start": "node src/bin/server", 11 | "dev": "nodemon src/bin/server", 12 | "db:load": "knex seed:run", 13 | "db:migrate": "knex migrate:latest", 14 | "db:rollback": "knex migrate:rollback", 15 | "db:currentVersion": "knex migrate:currentVersion", 16 | "lint": "eslint \"**/*.js\"", 17 | "format": "prettier --write \"**/*.js\"", 18 | "test": "jest --coverage --verbose", 19 | "test:watch": "jest --watch" 20 | }, 21 | "prettier": { 22 | "semi": false, 23 | "trailingComma": "all" 24 | }, 25 | "dependencies": { 26 | "bcryptjs": "^2.4.3", 27 | "config": "^3.0.1", 28 | "date-fns": "^1.30.1", 29 | "dotenv": "^6.2.0", 30 | "humps": "^2.0.0", 31 | "join-js": "^1.0.1", 32 | "jsonwebtoken": "^8.4.0", 33 | "kcors": "2.2.2", 34 | "knex": "0.21.1", 35 | "koa": "^2.6.2", 36 | "koa-bodyparser": "^4.2.1", 37 | "koa-helmet": "^4.0.0", 38 | "koa-jwt": "^3.5.1", 39 | "koa-logger": "^3.2.0", 40 | "koa-response-time": "^2.1.0", 41 | "koa-router": "^7.4.0", 42 | "koa-x-request-id": "^2.0.0", 43 | "lodash": "^4.17.11", 44 | "mississippi": "^3.0.0", 45 | "p-event": "^2.1.0", 46 | "pg": "8.0.3", 47 | "pino": "^5.10.6", 48 | "pino-colada": "^1.4.4", 49 | "qs": "^6.6.0", 50 | "request": "^2.88.0", 51 | "request-promise": "^4.2.0", 52 | "slug": "^0.9.3", 53 | "sqlite3": "4.1.1", 54 | "stoppable": "^1.1.0", 55 | "uuid": "^3.3.2", 56 | "validator": "^10.10.0", 57 | "yup": "^0.26.6" 58 | }, 59 | "devDependencies": { 60 | "eslint": "^5.12.0", 61 | "eslint-config-prettier": "^3.3.0", 62 | "eslint-config-standard": "^12.0.0", 63 | "eslint-plugin-import": "^2.14.0", 64 | "eslint-plugin-node": "^8.0.1", 65 | "eslint-plugin-promise": "^4.0.1", 66 | "eslint-plugin-standard": "^4.0.0", 67 | "faker": "^4.1.0", 68 | "jest": "^23.6.0", 69 | "lint-staged": "^8.1.0", 70 | "nodemon": "^1.18.9", 71 | "prettier": "^1.15.3" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/middleware/error-middleware.js: -------------------------------------------------------------------------------- 1 | const errors = require("../lib/errors") 2 | const _ = require("lodash") 3 | 4 | module.exports = async (ctx, next) => { 5 | try { 6 | await next() 7 | ctx.assert(ctx.response.body && Number(ctx.response.status) !== 404, 404) 8 | } catch (err) { 9 | ctx.type = "application/json" 10 | 11 | const status = 12 | err.status || 13 | err.statusCode || 14 | err.status_code || 15 | (err.output && err.output.statusCode) || 16 | (err.oauthError && err.oauthError.statusCode) || 17 | 500 18 | 19 | if (!ctx.response.body) { 20 | ctx.response.body = { errors: {} } 21 | } 22 | // ctx.app.emit('error', err, ctx); 23 | console.error(err) 24 | 25 | switch (true) { 26 | case err instanceof errors.ValidationError: 27 | ctx.body.errors = formatValidationError(err) 28 | ctx.status = _.defaultTo(status, 422) 29 | break 30 | 31 | case err.code === "SQLITE_CONSTRAINT": { 32 | let path = "unknown" 33 | 34 | if (Number(err.errno) === 19) { 35 | // SQLITE3 UNIQUE 36 | const idx = err.message.lastIndexOf(".") 37 | if (idx !== -1) { 38 | path = err.message.substring(idx + 1, err.message.length) 39 | ctx.body.errors[path] = ["has already been taken"] 40 | } 41 | } 42 | 43 | ctx.status = _.defaultTo(status, 422) 44 | break 45 | } 46 | 47 | case Number(err.code) === 23505: { 48 | // PG UNIQUE 49 | let path = "unknown" 50 | const [key] = err.detail.match(/\(.+?\)/g) 51 | if (key) { 52 | path = key.substr(1, key.length - 2) 53 | } 54 | 55 | ctx.body.errors[path] = ["has already been taken"] 56 | ctx.status = _.defaultTo(status, 422) 57 | break 58 | } 59 | 60 | default: 61 | ctx.status = _.defaultTo(status, 500) 62 | break 63 | } 64 | } 65 | } 66 | 67 | function formatValidationError(err) { 68 | const result = {} 69 | if (err.path) { 70 | result[err.path] = [_.defaultTo(err.message, "is not valid")] 71 | } 72 | if (err.inner && err.inner.length > 0) { 73 | err.inner 74 | .map(err => formatValidationError(err)) 75 | .reduce((prev, curr) => Object.assign(prev, curr), result) 76 | } 77 | return result 78 | } 79 | -------------------------------------------------------------------------------- /src/controllers/comments-controller.js: -------------------------------------------------------------------------------- 1 | const humps = require("humps") 2 | const uuid = require("uuid") 3 | const _ = require("lodash") 4 | const { getSelect } = require("../lib/utils") 5 | const joinJs = require("join-js").default 6 | const db = require("../lib/db") 7 | const { 8 | commentFields, 9 | userFields, 10 | relationsMaps, 11 | } = require("../lib/relations-map") 12 | 13 | module.exports = { 14 | async byComment(comment, ctx, next) { 15 | ctx.assert(comment, 404) 16 | 17 | comment = await db("comments") 18 | .first() 19 | .where({ id: comment }) 20 | 21 | ctx.assert(comment, 404) 22 | 23 | ctx.params.comment = comment 24 | 25 | return next() 26 | }, 27 | 28 | async get(ctx) { 29 | const { user } = ctx.state 30 | const { article } = ctx.params 31 | 32 | let comments = await db("comments") 33 | .select( 34 | ...getSelect("comments", "comment", commentFields), 35 | ...getSelect("users", "author", userFields), 36 | "followers.id as author_following", 37 | ) 38 | .where({ article: article.id }) 39 | .leftJoin("users", "comments.author", "users.id") 40 | .leftJoin("followers", function() { 41 | this.on("users.id", "=", "followers.user").onIn("followers.follower", [ 42 | user && user.id, 43 | ]) 44 | }) 45 | 46 | comments = joinJs 47 | .map(comments, relationsMaps, "commentMap", "comment_") 48 | .map(c => { 49 | delete c.author.id 50 | c.author.following = Boolean(c.author.following) 51 | return c 52 | }) 53 | 54 | ctx.body = { comments } 55 | }, 56 | 57 | async post(ctx) { 58 | const { body } = ctx.request 59 | const { user } = ctx.state 60 | const { article } = ctx.params 61 | let { comment = {} } = body 62 | 63 | const opts = { abortEarly: false } 64 | 65 | comment.id = uuid() 66 | comment.author = user.id 67 | comment.article = article.id 68 | 69 | comment = await ctx.app.schemas.comment.validate(comment, opts) 70 | 71 | await db("comments").insert(humps.decamelizeKeys(comment)) 72 | 73 | comment.author = _.pick(user, ["username", "bio", "image", "id"]) 74 | 75 | ctx.body = { comment } 76 | }, 77 | 78 | async del(ctx) { 79 | const { comment } = ctx.params 80 | 81 | await db("comments") 82 | .del() 83 | .where({ id: comment.id }) 84 | 85 | ctx.body = {} 86 | }, 87 | } 88 | -------------------------------------------------------------------------------- /src/controllers/profiles-controller.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash") 2 | const uuid = require("uuid") 3 | const { getSelect } = require("../lib/utils") 4 | const { userFields, relationsMaps } = require("../lib/relations-map") 5 | const joinJs = require("join-js").default 6 | const db = require("../lib/db") 7 | 8 | module.exports = { 9 | async byUsername(username, ctx, next) { 10 | ctx.assert(username, 404) 11 | 12 | const { user } = ctx.state 13 | 14 | ctx.params.profile = await db("users") 15 | .select( 16 | ...getSelect("users", "profile", userFields), 17 | "followers.id as profile_following", 18 | ) 19 | .where({ username }) 20 | .leftJoin("followers", function() { 21 | this.on("users.id", "=", "followers.user").onIn("followers.follower", [ 22 | user && user.id, 23 | ]) 24 | }) 25 | 26 | ctx.assert(ctx.params.profile && ctx.params.profile.length, 404) 27 | 28 | ctx.params.profile = joinJs.mapOne( 29 | ctx.params.profile, 30 | relationsMaps, 31 | "userMap", 32 | "profile_", 33 | ) 34 | 35 | await next() 36 | 37 | if (ctx.body.profile) { 38 | ctx.body.profile = _.omit(ctx.body.profile, "id") 39 | ctx.body.profile.following = Boolean(ctx.body.profile.following) 40 | } 41 | }, 42 | 43 | async get(ctx) { 44 | const { profile } = ctx.params 45 | ctx.body = { profile } 46 | }, 47 | 48 | follow: { 49 | async post(ctx) { 50 | const { profile } = ctx.params 51 | const { user } = ctx.state 52 | 53 | if (profile.following) { 54 | ctx.body = { profile } 55 | return 56 | } 57 | 58 | if (user.username !== profile.username) { 59 | const follow = { 60 | id: uuid(), 61 | user: profile.id, 62 | follower: user.id, 63 | } 64 | 65 | try { 66 | await db("followers").insert(follow) 67 | } catch (err) { 68 | ctx.assert( 69 | parseInt(err.errno, 10) === 19 && parseInt(err.code, 10) === 23505, 70 | err, 71 | ) 72 | } 73 | 74 | profile.following = true 75 | } 76 | 77 | ctx.body = { profile } 78 | }, 79 | 80 | async del(ctx) { 81 | const { profile } = ctx.params 82 | const { user } = ctx.state 83 | 84 | if (!profile.following) { 85 | ctx.body = { profile } 86 | return 87 | } 88 | 89 | await db("followers") 90 | .where({ user: profile.id, follower: user.id }) 91 | .del() 92 | 93 | profile.following = false 94 | 95 | ctx.body = { profile } 96 | }, 97 | }, 98 | } 99 | -------------------------------------------------------------------------------- /src/controllers/users-controller.js: -------------------------------------------------------------------------------- 1 | const humps = require("humps") 2 | const uuid = require("uuid") 3 | const _ = require("lodash") 4 | const bcrypt = require("bcryptjs") 5 | const { ValidationError } = require("../lib/errors") 6 | const { generateJWTforUser } = require("../lib/utils") 7 | const db = require("../lib/db") 8 | 9 | module.exports = { 10 | async get(ctx) { 11 | const user = generateJWTforUser(ctx.state.user) 12 | 13 | ctx.body = { user } 14 | }, 15 | 16 | async post(ctx) { 17 | const { body } = ctx.request 18 | let { user = {} } = body 19 | const opts = { abortEarly: false, context: { validatePassword: true } } 20 | 21 | user.id = uuid() 22 | 23 | user = await ctx.app.schemas.user.validate(user, opts) 24 | 25 | user.password = await bcrypt.hash(user.password, 10) 26 | 27 | await db("users").insert(humps.decamelizeKeys(user)) 28 | 29 | user = generateJWTforUser(user) 30 | 31 | ctx.body = { user: _.omit(user, ["password"]) } 32 | }, 33 | 34 | async put(ctx) { 35 | const { body } = ctx.request 36 | let { user: fields = {} } = body 37 | const opts = { abortEarly: false, context: { validatePassword: false } } 38 | 39 | if (fields.password) { 40 | opts.context.validatePassword = true 41 | } 42 | 43 | let user = Object.assign({}, ctx.state.user, fields) 44 | user = await ctx.app.schemas.user.validate(user, opts) 45 | 46 | if (fields.password) { 47 | user.password = await bcrypt.hash(user.password, 10) 48 | } 49 | 50 | user.updatedAt = new Date().toISOString() 51 | 52 | await db("users") 53 | .where({ id: user.id }) 54 | .update(humps.decamelizeKeys(user)) 55 | 56 | user = generateJWTforUser(user) 57 | 58 | ctx.body = { user: _.omit(user, ["password"]) } 59 | }, 60 | 61 | async login(ctx) { 62 | const { body } = ctx.request 63 | 64 | ctx.assert( 65 | _.isObject(body.user) && body.user.email && body.user.password, 66 | 422, 67 | new ValidationError(["malformed request"], "", "email or password"), 68 | ) 69 | 70 | let user = await db("users") 71 | .first() 72 | .where({ email: body.user.email }) 73 | 74 | ctx.assert( 75 | user, 76 | 401, 77 | new ValidationError(["is invalid"], "", "email or password"), 78 | ) 79 | 80 | const isValid = await bcrypt.compare(body.user.password, user.password) 81 | 82 | ctx.assert( 83 | isValid, 84 | 401, 85 | new ValidationError(["is invalid"], "", "email or password"), 86 | ) 87 | 88 | user = generateJWTforUser(user) 89 | 90 | ctx.body = { user: _.omit(user, ["password"]) } 91 | }, 92 | } 93 | -------------------------------------------------------------------------------- /src/migrations/20170422213822_init.js: -------------------------------------------------------------------------------- 1 | exports.up = function(knex) { 2 | return knex.schema 3 | 4 | .createTable("users", function(table) { 5 | table 6 | .uuid("id") 7 | .unique() 8 | .primary() 9 | .notNullable() 10 | table 11 | .string("email") 12 | .unique() 13 | .notNullable() 14 | table 15 | .string("username") 16 | .unique() 17 | .notNullable() 18 | table.string("image").defaultTo("") 19 | table.text("bio").defaultTo("") 20 | table.string("password").notNullable() 21 | table.timestamps(true, true) 22 | }) 23 | 24 | .createTable("articles", function(table) { 25 | table 26 | .uuid("id") 27 | .unique() 28 | .primary() 29 | .notNullable() 30 | table 31 | .string("slug") 32 | .unique() 33 | .notNullable() 34 | table.string("title").notNullable() 35 | table.text("body").notNullable() 36 | table.string("description").notNullable() 37 | table 38 | .integer("favorites_count") 39 | .notNullable() 40 | .defaultTo(0) 41 | table 42 | .uuid("author") 43 | .notNullable() 44 | .references("users.id") 45 | .onDelete("CASCADE") 46 | table.timestamps(true, true) 47 | }) 48 | 49 | .createTable("comments", function(table) { 50 | table 51 | .uuid("id") 52 | .unique() 53 | .primary() 54 | .notNullable() 55 | table.text("body").notNullable() 56 | table 57 | .uuid("author") 58 | .notNullable() 59 | .references("users.id") 60 | .onDelete("CASCADE") 61 | table 62 | .uuid("article") 63 | .notNullable() 64 | .references("articles.id") 65 | .onDelete("CASCADE") 66 | table.timestamps(true, true) 67 | }) 68 | 69 | .createTable("favorites", function(table) { 70 | table 71 | .uuid("id") 72 | .unique() 73 | .primary() 74 | .notNullable() 75 | table 76 | .uuid("user") 77 | .notNullable() 78 | .references("users.id") 79 | .onDelete("CASCADE") 80 | table 81 | .uuid("article") 82 | .notNullable() 83 | .references("articles.id") 84 | .onDelete("CASCADE") 85 | table.timestamps(true, true) 86 | }) 87 | 88 | .createTable("followers", function(table) { 89 | table 90 | .uuid("id") 91 | .unique() 92 | .primary() 93 | .notNullable() 94 | table 95 | .uuid("user") 96 | .notNullable() 97 | .references("users.id") 98 | .onDelete("CASCADE") 99 | table 100 | .uuid("follower") 101 | .notNullable() 102 | .references("users.id") 103 | .onDelete("CASCADE") 104 | table.unique(["user", "follower"]) 105 | table.timestamps(true, true) 106 | }) 107 | 108 | .createTable("tags", function(table) { 109 | table 110 | .uuid("id") 111 | .unique() 112 | .primary() 113 | .notNullable() 114 | table 115 | .string("name") 116 | .unique() 117 | .notNullable() 118 | table.timestamps(true, true) 119 | }) 120 | 121 | .createTable("articles_tags", function(table) { 122 | table 123 | .uuid("id") 124 | .unique() 125 | .primary() 126 | .notNullable() 127 | table 128 | .uuid("article") 129 | .notNullable() 130 | .references("articles.id") 131 | .onDelete("CASCADE") 132 | table 133 | .uuid("tag") 134 | .notNullable() 135 | .references("tags.id") 136 | .onDelete("CASCADE") 137 | table.unique(["tag", "article"]) 138 | table.timestamps(true, true) 139 | }) 140 | } 141 | 142 | exports.down = function(knex) { 143 | return knex.schema 144 | .dropTableIfExists("users") 145 | .dropTableIfExists("articles") 146 | .dropTableIfExists("comments") 147 | .dropTableIfExists("favorites") 148 | .dropTableIfExists("followers") 149 | .dropTableIfExists("tags") 150 | .dropTableIfExists("articles_tags") 151 | } 152 | -------------------------------------------------------------------------------- /src/controllers/articles-controller.js: -------------------------------------------------------------------------------- 1 | const slug = require("slug") 2 | const uuid = require("uuid") 3 | const humps = require("humps") 4 | const _ = require("lodash") 5 | const comments = require("./comments-controller") 6 | const { ValidationError } = require("../lib/errors") 7 | const db = require("../lib/db") 8 | const joinJs = require("join-js").default 9 | const { getSelect } = require("../lib/utils") 10 | const { 11 | articleFields, 12 | userFields, 13 | relationsMaps, 14 | } = require("../lib/relations-map") 15 | 16 | module.exports = { 17 | async bySlug(slug, ctx, next) { 18 | ctx.assert(slug, 404) 19 | 20 | const article = await db("articles") 21 | .first() 22 | .where({ slug }) 23 | 24 | ctx.assert(article, 404) 25 | 26 | const tagsRelations = await db("articles_tags") 27 | .select() 28 | .where({ article: article.id }) 29 | 30 | let tagList = [] 31 | 32 | if (tagsRelations && tagsRelations.length > 0) { 33 | tagList = await db("tags") 34 | .select() 35 | .whereIn( 36 | "id", 37 | tagsRelations.map(r => r.tag), 38 | ) 39 | 40 | tagList = tagList.map(t => t.name) 41 | } 42 | 43 | article.tagList = tagList 44 | 45 | article.favorited = false 46 | 47 | const author = await db("users") 48 | .first("username", "bio", "image", "id") 49 | .where({ id: article.author }) 50 | 51 | article.author = author 52 | 53 | article.author.following = false 54 | 55 | const { user } = ctx.state 56 | 57 | if (user && user.username !== article.author.username) { 58 | const res = await db("followers") 59 | .where({ user: article.author.id, follower: user.id }) 60 | .select() 61 | 62 | if (res.length > 0) { 63 | article.author.following = true 64 | } 65 | } 66 | 67 | let favorites = [] 68 | 69 | if (user) { 70 | favorites = await db("favorites") 71 | .where({ user: user.id, article: article.id }) 72 | .select() 73 | 74 | if (favorites.length > 0) { 75 | article.favorited = true 76 | } 77 | } 78 | 79 | ctx.params.article = article 80 | ctx.params.favorites = favorites 81 | ctx.params.author = author 82 | ctx.params.tagList = tagList 83 | ctx.params.tagsRelations = tagsRelations 84 | 85 | await next() 86 | 87 | delete ctx.params.author.id 88 | }, 89 | 90 | async get(ctx) { 91 | const { user } = ctx.state 92 | const { offset, limit, tag, author, favorited } = ctx.query 93 | 94 | let articlesQuery = db("articles") 95 | .select( 96 | ...getSelect("articles", "article", articleFields), 97 | ...getSelect("users", "author", userFields), 98 | ...getSelect("articles_tags", "tag", ["id"]), 99 | ...getSelect("tags", "tag", ["id", "name"]), 100 | "favorites.id as article_favorited", 101 | "followers.id as author_following", 102 | ) 103 | .limit(limit) 104 | .offset(offset) 105 | .orderBy("articles.created_at", "desc") 106 | 107 | let countQuery = db("articles").count() 108 | 109 | if (author && author.length > 0) { 110 | const subQuery = db("users") 111 | .select("id") 112 | .whereIn("username", author) 113 | 114 | articlesQuery = articlesQuery.andWhere("articles.author", "in", subQuery) 115 | countQuery = countQuery.andWhere("articles.author", "in", subQuery) 116 | } 117 | 118 | if (favorited && favorited.length > 0) { 119 | const subQuery = db("favorites") 120 | .select("article") 121 | .whereIn( 122 | "user", 123 | db("users") 124 | .select("id") 125 | .whereIn("username", favorited), 126 | ) 127 | 128 | articlesQuery = articlesQuery.andWhere("articles.id", "in", subQuery) 129 | countQuery = countQuery.andWhere("articles.id", "in", subQuery) 130 | } 131 | 132 | if (tag && tag.length > 0) { 133 | const subQuery = db("articles_tags") 134 | .select("article") 135 | .whereIn( 136 | "tag", 137 | db("tags") 138 | .select("id") 139 | .whereIn("name", tag), 140 | ) 141 | 142 | articlesQuery = articlesQuery.andWhere("articles.id", "in", subQuery) 143 | countQuery = countQuery.andWhere("articles.id", "in", subQuery) 144 | } 145 | 146 | articlesQuery = articlesQuery 147 | .leftJoin("users", "articles.author", "users.id") 148 | .leftJoin("articles_tags", "articles.id", "articles_tags.article") 149 | .leftJoin("tags", "articles_tags.tag", "tags.id") 150 | .leftJoin("favorites", function() { 151 | this.on("articles.id", "=", "favorites.article").onIn( 152 | "favorites.user", 153 | [user && user.id], 154 | ) 155 | }) 156 | .leftJoin("followers", function() { 157 | this.on( 158 | "articles.author", 159 | "=", 160 | "followers.user", 161 | ).onIn("followers.follower", [user && user.id]) 162 | }) 163 | 164 | let [articles, [countRes]] = await Promise.all([articlesQuery, countQuery]) 165 | 166 | articles = joinJs 167 | .map(articles, relationsMaps, "articleMap", "article_") 168 | .map(a => { 169 | a.favorited = Boolean(a.favorited) 170 | a.tagList = a.tagList.map(t => t.name) 171 | a.author.following = Boolean(a.author.following) 172 | delete a.author.id 173 | return a 174 | }) 175 | 176 | let articlesCount = countRes.count || countRes["count(*)"] 177 | articlesCount = Number(articlesCount) 178 | 179 | ctx.body = { articles, articlesCount } 180 | }, 181 | 182 | async getOne(ctx) { 183 | ctx.body = { article: ctx.params.article } 184 | }, 185 | 186 | async post(ctx) { 187 | const { body } = ctx.request 188 | let { article } = body 189 | let tags 190 | const opts = { abortEarly: false } 191 | 192 | article.id = uuid() 193 | article.author = ctx.state.user.id 194 | 195 | article = await ctx.app.schemas.article.validate(article, opts) 196 | 197 | article.slug = slug(_.get(article, "title", ""), { lower: true }) 198 | 199 | if (article.tagList && article.tagList.length > 0) { 200 | tags = await Promise.all( 201 | article.tagList 202 | .map(t => ({ id: uuid(), name: t })) 203 | .map(t => ctx.app.schemas.tag.validate(t, opts)), 204 | ) 205 | } 206 | 207 | try { 208 | await db("articles").insert( 209 | humps.decamelizeKeys(_.omit(article, ["tagList"])), 210 | ) 211 | } catch (err) { 212 | ctx.assert( 213 | parseInt(err.errno, 10) === 19 || parseInt(err.code, 10) === 23505, 214 | err, 215 | ) 216 | 217 | article.slug = article.slug + "-" + uuid().substr(-6) 218 | 219 | await db("articles").insert( 220 | humps.decamelizeKeys(_.omit(article, ["tagList"])), 221 | ) 222 | } 223 | 224 | if (tags && tags.length) { 225 | for (var i = 0; i < tags.length; i++) { 226 | try { 227 | await db("tags").insert(humps.decamelizeKeys(tags[i])) 228 | } catch (err) { 229 | ctx.assert( 230 | parseInt(err.errno, 10) === 19 || parseInt(err.code, 10) === 23505, 231 | err, 232 | ) 233 | } 234 | } 235 | 236 | tags = await db("tags") 237 | .select() 238 | .whereIn( 239 | "name", 240 | tags.map(t => t.name), 241 | ) 242 | 243 | const relations = tags.map(t => ({ 244 | id: uuid(), 245 | tag: t.id, 246 | article: article.id, 247 | })) 248 | 249 | await db("articles_tags").insert(relations) 250 | } 251 | 252 | article.favorited = false 253 | article.author = _.pick(ctx.state.user, ["username", "bio", "image"]) 254 | article.author.following = false 255 | 256 | ctx.body = { article } 257 | }, 258 | 259 | async put(ctx) { 260 | const { article } = ctx.params 261 | 262 | ctx.assert( 263 | article.author.id === ctx.state.user.id, 264 | 422, 265 | new ValidationError(["not owned by user"], "", "article"), 266 | ) 267 | 268 | const { body } = ctx.request 269 | let { article: fields = {} } = body 270 | const opts = { abortEarly: false } 271 | 272 | let newArticle = Object.assign({}, article, fields) 273 | newArticle.author = newArticle.author.id 274 | newArticle = await ctx.app.schemas.article.validate( 275 | humps.camelizeKeys(newArticle), 276 | opts, 277 | ) 278 | 279 | if (fields.title) { 280 | newArticle.slug = slug(_.get(newArticle, "title", ""), { lower: true }) 281 | } 282 | 283 | newArticle.updatedAt = new Date().toISOString() 284 | 285 | try { 286 | await db("articles") 287 | .update( 288 | humps.decamelizeKeys( 289 | _.pick(newArticle, [ 290 | "title", 291 | "slug", 292 | "body", 293 | "description", 294 | "updatedAt", 295 | ]), 296 | ), 297 | ) 298 | .where({ id: article.id }) 299 | } catch (err) { 300 | ctx.assert( 301 | parseInt(err.errno, 10) === 19 || parseInt(err.code, 10) === 23505, 302 | err, 303 | ) 304 | 305 | newArticle.slug = newArticle.slug + "-" + uuid().substr(-6) 306 | 307 | await db("articles") 308 | .update( 309 | humps.decamelizeKeys( 310 | _.pick(newArticle, [ 311 | "title", 312 | "slug", 313 | "body", 314 | "description", 315 | "updatedAt", 316 | ]), 317 | ), 318 | ) 319 | .where({ id: article.id }) 320 | } 321 | 322 | if (fields.tagList && fields.tagList.length === 0) { 323 | await db("articles_tags") 324 | .del() 325 | .where({ article: article.id }) 326 | } 327 | 328 | if (fields.tagList && fields.tagList.length > 0) { 329 | if ( 330 | _.difference(article.tagList).length || 331 | _.difference(fields.tagList).length 332 | ) { 333 | await db("articles_tags") 334 | .del() 335 | .where({ article: article.id }) 336 | 337 | let tags = await Promise.all( 338 | newArticle.tagList 339 | .map(t => ({ id: uuid(), name: t })) 340 | .map(t => ctx.app.schemas.tag.validate(t, opts)), 341 | ) 342 | 343 | for (var i = 0; i < tags.length; i++) { 344 | try { 345 | await db("tags").insert(humps.decamelizeKeys(tags[i])) 346 | } catch (err) { 347 | ctx.assert( 348 | parseInt(err.errno, 10) === 19 || 349 | parseInt(err.code, 10) === 23505, 350 | err, 351 | ) 352 | } 353 | } 354 | 355 | tags = await db("tags") 356 | .select() 357 | .whereIn( 358 | "name", 359 | tags.map(t => t.name), 360 | ) 361 | 362 | const relations = tags.map(t => ({ 363 | id: uuid(), 364 | tag: t.id, 365 | article: article.id, 366 | })) 367 | 368 | await db("articles_tags").insert(relations) 369 | } 370 | } 371 | 372 | newArticle.author = ctx.params.author 373 | newArticle.favorited = article.favorited 374 | ctx.body = { article: newArticle } 375 | }, 376 | 377 | async del(ctx) { 378 | const { article } = ctx.params 379 | 380 | ctx.assert( 381 | article.author.id === ctx.state.user.id, 382 | 422, 383 | new ValidationError(["not owned by user"], "", "article"), 384 | ) 385 | 386 | await Promise.all([ 387 | db("favorites") 388 | .del() 389 | .where({ user: ctx.state.user.id, article: article.id }), 390 | 391 | db("articles_tags") 392 | .del() 393 | .where({ article: article.id }), 394 | 395 | db("articles") 396 | .del() 397 | .where({ id: article.id }), 398 | ]) 399 | 400 | ctx.body = {} 401 | }, 402 | 403 | feed: { 404 | async get(ctx) { 405 | const { user } = ctx.state 406 | const { offset, limit } = ctx.query 407 | 408 | const followedIds = await db("followers") 409 | .pluck("user") 410 | .where({ follower: user.id }) 411 | 412 | let [articles, [countRes]] = await Promise.all([ 413 | db("articles") 414 | .select( 415 | ...getSelect("articles", "article", articleFields), 416 | ...getSelect("users", "author", userFields), 417 | ...getSelect("articles_tags", "tag", ["id"]), 418 | ...getSelect("tags", "tag", ["id", "name"]), 419 | "favorites.id as article_favorited", 420 | ) 421 | .whereIn("articles.author", followedIds) 422 | .limit(limit) 423 | .offset(offset) 424 | .orderBy("articles.created_at", "desc") 425 | .leftJoin("users", "articles.author", "users.id") 426 | .leftJoin("articles_tags", "articles.id", "articles_tags.article") 427 | .leftJoin("tags", "articles_tags.tag", "tags.id") 428 | .leftJoin("favorites", function() { 429 | this.on( 430 | "articles.id", 431 | "=", 432 | "favorites.article", 433 | ).onIn("favorites.user", [user && user.id]) 434 | }), 435 | 436 | db("articles") 437 | .count() 438 | .whereIn("author", followedIds), 439 | ]) 440 | 441 | articles = joinJs 442 | .map(articles, relationsMaps, "articleMap", "article_") 443 | .map(a => { 444 | a.favorited = Boolean(a.favorited) 445 | a.tagList = a.tagList.map(t => t.name) 446 | a.author.following = true 447 | delete a.author.id 448 | return a 449 | }) 450 | 451 | let articlesCount = countRes.count || countRes["count(*)"] 452 | articlesCount = Number(articlesCount) 453 | 454 | ctx.body = { articles, articlesCount } 455 | }, 456 | }, 457 | 458 | favorite: { 459 | async post(ctx) { 460 | const { article } = ctx.params 461 | 462 | if (article.favorited) { 463 | ctx.body = { article: ctx.params.article } 464 | return 465 | } 466 | 467 | await Promise.all([ 468 | db("favorites").insert({ 469 | id: uuid(), 470 | user: ctx.state.user.id, 471 | article: article.id, 472 | }), 473 | db("articles") 474 | .increment("favorites_count", 1) 475 | .where({ id: article.id }), 476 | ]) 477 | 478 | article.favorited = true 479 | article.favorites_count = Number(article.favorites_count) + 1 480 | 481 | ctx.body = { article: ctx.params.article } 482 | }, 483 | 484 | async del(ctx) { 485 | const { article } = ctx.params 486 | 487 | if (!article.favorited) { 488 | ctx.body = { article: ctx.params.article } 489 | return 490 | } 491 | 492 | await Promise.all([ 493 | db("favorites") 494 | .del() 495 | .where({ user: ctx.state.user.id, article: article.id }), 496 | db("articles") 497 | .decrement("favorites_count", 1) 498 | .where({ id: article.id }), 499 | ]) 500 | 501 | article.favorited = false 502 | article.favorites_count = Number(article.favorites_count) - 1 503 | 504 | ctx.body = { article: ctx.params.article } 505 | }, 506 | }, 507 | 508 | comments, 509 | } 510 | -------------------------------------------------------------------------------- /Conduit.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "0574ad8a-a525-43ae-8e1e-5fd9756037f4", 4 | "name": "Conduit", 5 | "description": "Collection for testing the Conduit API\n\nhttps://github.com/gothinkster/realworld", 6 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Auth", 11 | "item": [ 12 | { 13 | "name": "Register", 14 | "event": [ 15 | { 16 | "listen": "test", 17 | "script": { 18 | "type": "text/javascript", 19 | "exec": [ 20 | "if (!(environment.isIntegrationTest)) {", 21 | "var responseJSON = JSON.parse(responseBody);", 22 | "", 23 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 24 | "", 25 | "var user = responseJSON.user || {};", 26 | "", 27 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 28 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 29 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 30 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 31 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 32 | "}", 33 | "" 34 | ] 35 | } 36 | } 37 | ], 38 | "request": { 39 | "method": "POST", 40 | "header": [ 41 | { 42 | "key": "Content-Type", 43 | "value": "application/json" 44 | }, 45 | { 46 | "key": "X-Requested-With", 47 | "value": "XMLHttpRequest" 48 | } 49 | ], 50 | "body": { 51 | "mode": "raw", 52 | "raw": "{\"user\":{\"email\":\"{{EMAIL}}\", \"password\":\"{{PASSWORD}}\", \"username\":\"{{USERNAME}}\"}}" 53 | }, 54 | "url": { 55 | "raw": "{{APIURL}}/users", 56 | "host": ["{{APIURL}}"], 57 | "path": ["users"] 58 | } 59 | }, 60 | "response": [] 61 | }, 62 | { 63 | "name": "Login", 64 | "event": [ 65 | { 66 | "listen": "test", 67 | "script": { 68 | "type": "text/javascript", 69 | "exec": [ 70 | "var responseJSON = JSON.parse(responseBody);", 71 | "", 72 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 73 | "", 74 | "var user = responseJSON.user || {};", 75 | "", 76 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 77 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 78 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 79 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 80 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 81 | "" 82 | ] 83 | } 84 | } 85 | ], 86 | "request": { 87 | "method": "POST", 88 | "header": [ 89 | { 90 | "key": "Content-Type", 91 | "value": "application/json" 92 | }, 93 | { 94 | "key": "X-Requested-With", 95 | "value": "XMLHttpRequest" 96 | } 97 | ], 98 | "body": { 99 | "mode": "raw", 100 | "raw": "{\"user\":{\"email\":\"{{EMAIL}}\", \"password\":\"{{PASSWORD}}\"}}" 101 | }, 102 | "url": { 103 | "raw": "{{APIURL}}/users/login", 104 | "host": ["{{APIURL}}"], 105 | "path": ["users", "login"] 106 | } 107 | }, 108 | "response": [] 109 | }, 110 | { 111 | "name": "Login and Remember Token", 112 | "event": [ 113 | { 114 | "listen": "test", 115 | "script": { 116 | "id": "a7674032-bf09-4ae7-8224-4afa2fb1a9f9", 117 | "type": "text/javascript", 118 | "exec": [ 119 | "var responseJSON = JSON.parse(responseBody);", 120 | "", 121 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 122 | "", 123 | "var user = responseJSON.user || {};", 124 | "", 125 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 126 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 127 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 128 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 129 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 130 | "", 131 | "if(tests['User has \"token\" property']){", 132 | " pm.globals.set('token', user.token);", 133 | "}", 134 | "", 135 | "tests['Global variable \"token\" has been set'] = pm.globals.get('token') === user.token;", 136 | "" 137 | ] 138 | } 139 | } 140 | ], 141 | "request": { 142 | "method": "POST", 143 | "header": [ 144 | { 145 | "key": "Content-Type", 146 | "value": "application/json" 147 | }, 148 | { 149 | "key": "X-Requested-With", 150 | "value": "XMLHttpRequest" 151 | } 152 | ], 153 | "body": { 154 | "mode": "raw", 155 | "raw": "{\"user\":{\"email\":\"{{EMAIL}}\", \"password\":\"{{PASSWORD}}\"}}" 156 | }, 157 | "url": { 158 | "raw": "{{APIURL}}/users/login", 159 | "host": ["{{APIURL}}"], 160 | "path": ["users", "login"] 161 | } 162 | }, 163 | "response": [] 164 | }, 165 | { 166 | "name": "Current User", 167 | "event": [ 168 | { 169 | "listen": "test", 170 | "script": { 171 | "type": "text/javascript", 172 | "exec": [ 173 | "var responseJSON = JSON.parse(responseBody);", 174 | "", 175 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 176 | "", 177 | "var user = responseJSON.user || {};", 178 | "", 179 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 180 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 181 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 182 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 183 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 184 | "" 185 | ] 186 | } 187 | } 188 | ], 189 | "request": { 190 | "method": "GET", 191 | "header": [ 192 | { 193 | "key": "Content-Type", 194 | "value": "application/json" 195 | }, 196 | { 197 | "key": "X-Requested-With", 198 | "value": "XMLHttpRequest" 199 | }, 200 | { 201 | "key": "Authorization", 202 | "value": "Token {{token}}" 203 | } 204 | ], 205 | "body": { 206 | "mode": "raw", 207 | "raw": "" 208 | }, 209 | "url": { 210 | "raw": "{{APIURL}}/user", 211 | "host": ["{{APIURL}}"], 212 | "path": ["user"] 213 | } 214 | }, 215 | "response": [] 216 | }, 217 | { 218 | "name": "Update User", 219 | "event": [ 220 | { 221 | "listen": "test", 222 | "script": { 223 | "type": "text/javascript", 224 | "exec": [ 225 | "var responseJSON = JSON.parse(responseBody);", 226 | "", 227 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 228 | "", 229 | "var user = responseJSON.user || {};", 230 | "", 231 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 232 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 233 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 234 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 235 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 236 | "" 237 | ] 238 | } 239 | } 240 | ], 241 | "request": { 242 | "method": "PUT", 243 | "header": [ 244 | { 245 | "key": "Content-Type", 246 | "value": "application/json" 247 | }, 248 | { 249 | "key": "X-Requested-With", 250 | "value": "XMLHttpRequest" 251 | }, 252 | { 253 | "key": "Authorization", 254 | "value": "Token {{token}}" 255 | } 256 | ], 257 | "body": { 258 | "mode": "raw", 259 | "raw": "{\"user\":{\"email\":\"{{EMAIL}}\"}}" 260 | }, 261 | "url": { 262 | "raw": "{{APIURL}}/user", 263 | "host": ["{{APIURL}}"], 264 | "path": ["user"] 265 | } 266 | }, 267 | "response": [] 268 | } 269 | ] 270 | }, 271 | { 272 | "name": "Articles", 273 | "item": [ 274 | { 275 | "name": "All Articles", 276 | "event": [ 277 | { 278 | "listen": "test", 279 | "script": { 280 | "type": "text/javascript", 281 | "exec": [ 282 | "var is200Response = responseCode.code === 200;", 283 | "", 284 | "tests['Response code is 200 OK'] = is200Response;", 285 | "", 286 | "if(is200Response){", 287 | " var responseJSON = JSON.parse(responseBody);", 288 | "", 289 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 290 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 291 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 292 | "", 293 | " if(responseJSON.articles.length){", 294 | " var article = responseJSON.articles[0];", 295 | "", 296 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 297 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 298 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 299 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 300 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 301 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 302 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 303 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 304 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 305 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 306 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 307 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 308 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 309 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 310 | " } else {", 311 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 312 | " }", 313 | "}", 314 | "" 315 | ] 316 | } 317 | } 318 | ], 319 | "request": { 320 | "method": "GET", 321 | "header": [ 322 | { 323 | "key": "Content-Type", 324 | "value": "application/json" 325 | }, 326 | { 327 | "key": "X-Requested-With", 328 | "value": "XMLHttpRequest" 329 | } 330 | ], 331 | "body": { 332 | "mode": "raw", 333 | "raw": "" 334 | }, 335 | "url": { 336 | "raw": "{{APIURL}}/articles", 337 | "host": ["{{APIURL}}"], 338 | "path": ["articles"] 339 | } 340 | }, 341 | "response": [] 342 | }, 343 | { 344 | "name": "Articles by Author", 345 | "event": [ 346 | { 347 | "listen": "test", 348 | "script": { 349 | "type": "text/javascript", 350 | "exec": [ 351 | "var is200Response = responseCode.code === 200;", 352 | "", 353 | "tests['Response code is 200 OK'] = is200Response;", 354 | "", 355 | "if(is200Response){", 356 | " var responseJSON = JSON.parse(responseBody);", 357 | "", 358 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 359 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 360 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 361 | "", 362 | " if(responseJSON.articles.length){", 363 | " var article = responseJSON.articles[0];", 364 | "", 365 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 366 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 367 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 368 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 369 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 370 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 371 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 372 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 373 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 374 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 375 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 376 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 377 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 378 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 379 | " } else {", 380 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 381 | " }", 382 | "}", 383 | "" 384 | ] 385 | } 386 | } 387 | ], 388 | "request": { 389 | "method": "GET", 390 | "header": [ 391 | { 392 | "key": "Content-Type", 393 | "value": "application/json" 394 | }, 395 | { 396 | "key": "X-Requested-With", 397 | "value": "XMLHttpRequest" 398 | } 399 | ], 400 | "body": { 401 | "mode": "raw", 402 | "raw": "" 403 | }, 404 | "url": { 405 | "raw": "{{APIURL}}/articles?author=johnjacob", 406 | "host": ["{{APIURL}}"], 407 | "path": ["articles"], 408 | "query": [ 409 | { 410 | "key": "author", 411 | "value": "johnjacob" 412 | } 413 | ] 414 | } 415 | }, 416 | "response": [] 417 | }, 418 | { 419 | "name": "Articles Favorited by Username", 420 | "event": [ 421 | { 422 | "listen": "test", 423 | "script": { 424 | "type": "text/javascript", 425 | "exec": [ 426 | "var is200Response = responseCode.code === 200;", 427 | "", 428 | "tests['Response code is 200 OK'] = is200Response;", 429 | "", 430 | "if(is200Response){", 431 | " var responseJSON = JSON.parse(responseBody);", 432 | " ", 433 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 434 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 435 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 436 | "", 437 | " if(responseJSON.articles.length){", 438 | " var article = responseJSON.articles[0];", 439 | "", 440 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 441 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 442 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 443 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 444 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 445 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 446 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 447 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 448 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 449 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 450 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 451 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 452 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 453 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 454 | " } else {", 455 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 456 | " }", 457 | "}", 458 | "" 459 | ] 460 | } 461 | } 462 | ], 463 | "request": { 464 | "method": "GET", 465 | "header": [ 466 | { 467 | "key": "Content-Type", 468 | "value": "application/json" 469 | }, 470 | { 471 | "key": "X-Requested-With", 472 | "value": "XMLHttpRequest" 473 | } 474 | ], 475 | "body": { 476 | "mode": "raw", 477 | "raw": "" 478 | }, 479 | "url": { 480 | "raw": "{{APIURL}}/articles?favorited=jane", 481 | "host": ["{{APIURL}}"], 482 | "path": ["articles"], 483 | "query": [ 484 | { 485 | "key": "favorited", 486 | "value": "jane" 487 | } 488 | ] 489 | } 490 | }, 491 | "response": [] 492 | }, 493 | { 494 | "name": "Articles by Tag", 495 | "event": [ 496 | { 497 | "listen": "test", 498 | "script": { 499 | "type": "text/javascript", 500 | "exec": [ 501 | "var is200Response = responseCode.code === 200;", 502 | "", 503 | "tests['Response code is 200 OK'] = is200Response;", 504 | "", 505 | "if(is200Response){", 506 | " var responseJSON = JSON.parse(responseBody);", 507 | "", 508 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 509 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 510 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 511 | "", 512 | " if(responseJSON.articles.length){", 513 | " var article = responseJSON.articles[0];", 514 | "", 515 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 516 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 517 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 518 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 519 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 520 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 521 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 522 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 523 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 524 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 525 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 526 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 527 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 528 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 529 | " } else {", 530 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 531 | " }", 532 | "}", 533 | "" 534 | ] 535 | } 536 | } 537 | ], 538 | "request": { 539 | "method": "GET", 540 | "header": [ 541 | { 542 | "key": "Content-Type", 543 | "value": "application/json" 544 | }, 545 | { 546 | "key": "X-Requested-With", 547 | "value": "XMLHttpRequest" 548 | } 549 | ], 550 | "body": { 551 | "mode": "raw", 552 | "raw": "" 553 | }, 554 | "url": { 555 | "raw": "{{APIURL}}/articles?tag=dragons", 556 | "host": ["{{APIURL}}"], 557 | "path": ["articles"], 558 | "query": [ 559 | { 560 | "key": "tag", 561 | "value": "dragons" 562 | } 563 | ] 564 | } 565 | }, 566 | "response": [] 567 | } 568 | ] 569 | }, 570 | { 571 | "name": "Articles, Favorite, Comments", 572 | "item": [ 573 | { 574 | "name": "Create Article", 575 | "event": [ 576 | { 577 | "listen": "test", 578 | "script": { 579 | "id": "e711dbf8-8065-4ba8-8b74-f1639a7d8208", 580 | "type": "text/javascript", 581 | "exec": [ 582 | "var responseJSON = JSON.parse(responseBody);", 583 | "", 584 | "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');", 585 | "", 586 | "var article = responseJSON.article || {};", 587 | "", 588 | "tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 589 | "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 590 | "pm.globals.set('slug', article.slug);", 591 | "", 592 | "tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 593 | "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 594 | "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 595 | "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 596 | "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 597 | "tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 598 | "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 599 | "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 600 | "tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 601 | "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 602 | "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 603 | "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 604 | "" 605 | ] 606 | } 607 | } 608 | ], 609 | "request": { 610 | "method": "POST", 611 | "header": [ 612 | { 613 | "key": "Content-Type", 614 | "value": "application/json" 615 | }, 616 | { 617 | "key": "X-Requested-With", 618 | "value": "XMLHttpRequest" 619 | }, 620 | { 621 | "key": "Authorization", 622 | "value": "Token {{token}}" 623 | } 624 | ], 625 | "body": { 626 | "mode": "raw", 627 | "raw": "{\"article\":{\"title\":\"How to train your dragon\", \"description\":\"Ever wonder how?\", \"body\":\"Very carefully.\", \"tagList\":[\"dragons\",\"training\"]}}" 628 | }, 629 | "url": { 630 | "raw": "{{APIURL}}/articles", 631 | "host": ["{{APIURL}}"], 632 | "path": ["articles"] 633 | } 634 | }, 635 | "response": [] 636 | }, 637 | { 638 | "name": "Feed", 639 | "event": [ 640 | { 641 | "listen": "test", 642 | "script": { 643 | "type": "text/javascript", 644 | "exec": [ 645 | "var is200Response = responseCode.code === 200;", 646 | "", 647 | "tests['Response code is 200 OK'] = is200Response;", 648 | "", 649 | "if(is200Response){", 650 | " var responseJSON = JSON.parse(responseBody);", 651 | "", 652 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 653 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 654 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 655 | "", 656 | " if(responseJSON.articles.length){", 657 | " var article = responseJSON.articles[0];", 658 | "", 659 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 660 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 661 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 662 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 663 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 664 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 665 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 666 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 667 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 668 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 669 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 670 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 671 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 672 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 673 | " } else {", 674 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 675 | " }", 676 | "}", 677 | "" 678 | ] 679 | } 680 | } 681 | ], 682 | "request": { 683 | "method": "GET", 684 | "header": [ 685 | { 686 | "key": "Content-Type", 687 | "value": "application/json" 688 | }, 689 | { 690 | "key": "X-Requested-With", 691 | "value": "XMLHttpRequest" 692 | }, 693 | { 694 | "key": "Authorization", 695 | "value": "Token {{token}}" 696 | } 697 | ], 698 | "body": { 699 | "mode": "raw", 700 | "raw": "" 701 | }, 702 | "url": { 703 | "raw": "{{APIURL}}/articles/feed", 704 | "host": ["{{APIURL}}"], 705 | "path": ["articles", "feed"] 706 | } 707 | }, 708 | "response": [] 709 | }, 710 | { 711 | "name": "All Articles", 712 | "event": [ 713 | { 714 | "listen": "test", 715 | "script": { 716 | "type": "text/javascript", 717 | "exec": [ 718 | "var is200Response = responseCode.code === 200;", 719 | "", 720 | "tests['Response code is 200 OK'] = is200Response;", 721 | "", 722 | "if(is200Response){", 723 | " var responseJSON = JSON.parse(responseBody);", 724 | "", 725 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 726 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 727 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 728 | "", 729 | " if(responseJSON.articles.length){", 730 | " var article = responseJSON.articles[0];", 731 | "", 732 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 733 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 734 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 735 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 736 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 737 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 738 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 739 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 740 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 741 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 742 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 743 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 744 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 745 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 746 | " } else {", 747 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 748 | " }", 749 | "}", 750 | "" 751 | ] 752 | } 753 | } 754 | ], 755 | "request": { 756 | "method": "GET", 757 | "header": [ 758 | { 759 | "key": "Content-Type", 760 | "value": "application/json" 761 | }, 762 | { 763 | "key": "X-Requested-With", 764 | "value": "XMLHttpRequest" 765 | }, 766 | { 767 | "key": "Authorization", 768 | "value": "Token {{token}}" 769 | } 770 | ], 771 | "body": { 772 | "mode": "raw", 773 | "raw": "" 774 | }, 775 | "url": { 776 | "raw": "{{APIURL}}/articles", 777 | "host": ["{{APIURL}}"], 778 | "path": ["articles"] 779 | } 780 | }, 781 | "response": [] 782 | }, 783 | { 784 | "name": "All Articles with auth", 785 | "event": [ 786 | { 787 | "listen": "test", 788 | "script": { 789 | "type": "text/javascript", 790 | "exec": [ 791 | "var is200Response = responseCode.code === 200;", 792 | "", 793 | "tests['Response code is 200 OK'] = is200Response;", 794 | "", 795 | "if(is200Response){", 796 | " var responseJSON = JSON.parse(responseBody);", 797 | "", 798 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 799 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 800 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 801 | "", 802 | " if(responseJSON.articles.length){", 803 | " var article = responseJSON.articles[0];", 804 | "", 805 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 806 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 807 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 808 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 809 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 810 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 811 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 812 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 813 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 814 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 815 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 816 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 817 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 818 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 819 | " } else {", 820 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 821 | " }", 822 | "}", 823 | "" 824 | ] 825 | } 826 | } 827 | ], 828 | "request": { 829 | "method": "GET", 830 | "header": [ 831 | { 832 | "key": "Content-Type", 833 | "value": "application/json" 834 | }, 835 | { 836 | "key": "X-Requested-With", 837 | "value": "XMLHttpRequest" 838 | }, 839 | { 840 | "key": "Authorization", 841 | "value": "Token {{token}}" 842 | } 843 | ], 844 | "body": { 845 | "mode": "raw", 846 | "raw": "" 847 | }, 848 | "url": { 849 | "raw": "{{APIURL}}/articles", 850 | "host": ["{{APIURL}}"], 851 | "path": ["articles"] 852 | } 853 | }, 854 | "response": [] 855 | }, 856 | { 857 | "name": "Articles by Author", 858 | "event": [ 859 | { 860 | "listen": "test", 861 | "script": { 862 | "type": "text/javascript", 863 | "exec": [ 864 | "var is200Response = responseCode.code === 200;", 865 | "", 866 | "tests['Response code is 200 OK'] = is200Response;", 867 | "", 868 | "if(is200Response){", 869 | " var responseJSON = JSON.parse(responseBody);", 870 | "", 871 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 872 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 873 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 874 | "", 875 | " if(responseJSON.articles.length){", 876 | " var article = responseJSON.articles[0];", 877 | "", 878 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 879 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 880 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 881 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 882 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 883 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 884 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 885 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 886 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 887 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 888 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 889 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 890 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 891 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 892 | " } else {", 893 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 894 | " }", 895 | "}", 896 | "" 897 | ] 898 | } 899 | } 900 | ], 901 | "request": { 902 | "method": "GET", 903 | "header": [ 904 | { 905 | "key": "Content-Type", 906 | "value": "application/json" 907 | }, 908 | { 909 | "key": "X-Requested-With", 910 | "value": "XMLHttpRequest" 911 | }, 912 | { 913 | "key": "Authorization", 914 | "value": "Token {{token}}" 915 | } 916 | ], 917 | "body": { 918 | "mode": "raw", 919 | "raw": "" 920 | }, 921 | "url": { 922 | "raw": "{{APIURL}}/articles?author={{USERNAME}}", 923 | "host": ["{{APIURL}}"], 924 | "path": ["articles"], 925 | "query": [ 926 | { 927 | "key": "author", 928 | "value": "{{USERNAME}}" 929 | } 930 | ] 931 | } 932 | }, 933 | "response": [] 934 | }, 935 | { 936 | "name": "Articles by Author with auth", 937 | "event": [ 938 | { 939 | "listen": "test", 940 | "script": { 941 | "type": "text/javascript", 942 | "exec": [ 943 | "var is200Response = responseCode.code === 200;", 944 | "", 945 | "tests['Response code is 200 OK'] = is200Response;", 946 | "", 947 | "if(is200Response){", 948 | " var responseJSON = JSON.parse(responseBody);", 949 | "", 950 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 951 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 952 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 953 | "", 954 | " if(responseJSON.articles.length){", 955 | " var article = responseJSON.articles[0];", 956 | "", 957 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 958 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 959 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 960 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 961 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 962 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 963 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 964 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 965 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 966 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 967 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 968 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 969 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 970 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 971 | " } else {", 972 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 973 | " }", 974 | "}", 975 | "" 976 | ] 977 | } 978 | } 979 | ], 980 | "request": { 981 | "method": "GET", 982 | "header": [ 983 | { 984 | "key": "Content-Type", 985 | "value": "application/json" 986 | }, 987 | { 988 | "key": "X-Requested-With", 989 | "value": "XMLHttpRequest" 990 | }, 991 | { 992 | "key": "Authorization", 993 | "value": "Token {{token}}" 994 | } 995 | ], 996 | "body": { 997 | "mode": "raw", 998 | "raw": "" 999 | }, 1000 | "url": { 1001 | "raw": "{{APIURL}}/articles?author={{USERNAME}}", 1002 | "host": ["{{APIURL}}"], 1003 | "path": ["articles"], 1004 | "query": [ 1005 | { 1006 | "key": "author", 1007 | "value": "{{USERNAME}}" 1008 | } 1009 | ] 1010 | } 1011 | }, 1012 | "response": [] 1013 | }, 1014 | { 1015 | "name": "Articles Favorited by Username", 1016 | "event": [ 1017 | { 1018 | "listen": "test", 1019 | "script": { 1020 | "type": "text/javascript", 1021 | "exec": [ 1022 | "var is200Response = responseCode.code === 200;", 1023 | "", 1024 | "tests['Response code is 200 OK'] = is200Response;", 1025 | "", 1026 | "if(is200Response){", 1027 | " var responseJSON = JSON.parse(responseBody);", 1028 | " ", 1029 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 1030 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 1031 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 1032 | "", 1033 | " if(responseJSON.articles.length){", 1034 | " var article = responseJSON.articles[0];", 1035 | "", 1036 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1037 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1038 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1039 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1040 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1041 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1042 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1043 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1044 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1045 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1046 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1047 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1048 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1049 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1050 | " } else {", 1051 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 1052 | " }", 1053 | "}", 1054 | "" 1055 | ] 1056 | } 1057 | } 1058 | ], 1059 | "request": { 1060 | "method": "GET", 1061 | "header": [ 1062 | { 1063 | "key": "Content-Type", 1064 | "value": "application/json" 1065 | }, 1066 | { 1067 | "key": "X-Requested-With", 1068 | "value": "XMLHttpRequest" 1069 | }, 1070 | { 1071 | "key": "Authorization", 1072 | "value": "Token {{token}}" 1073 | } 1074 | ], 1075 | "body": { 1076 | "mode": "raw", 1077 | "raw": "" 1078 | }, 1079 | "url": { 1080 | "raw": "{{APIURL}}/articles?favorited=jane", 1081 | "host": ["{{APIURL}}"], 1082 | "path": ["articles"], 1083 | "query": [ 1084 | { 1085 | "key": "favorited", 1086 | "value": "jane" 1087 | } 1088 | ] 1089 | } 1090 | }, 1091 | "response": [] 1092 | }, 1093 | { 1094 | "name": "Articles Favorited by Username with auth", 1095 | "event": [ 1096 | { 1097 | "listen": "test", 1098 | "script": { 1099 | "type": "text/javascript", 1100 | "exec": [ 1101 | "var is200Response = responseCode.code === 200;", 1102 | "", 1103 | "tests['Response code is 200 OK'] = is200Response;", 1104 | "", 1105 | "if(is200Response){", 1106 | " var responseJSON = JSON.parse(responseBody);", 1107 | " ", 1108 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 1109 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 1110 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 1111 | "", 1112 | " if(responseJSON.articles.length){", 1113 | " var article = responseJSON.articles[0];", 1114 | "", 1115 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1116 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1117 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1118 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1119 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1120 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1121 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1122 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1123 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1124 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1125 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1126 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1127 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1128 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1129 | " } else {", 1130 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 1131 | " }", 1132 | "}", 1133 | "" 1134 | ] 1135 | } 1136 | } 1137 | ], 1138 | "request": { 1139 | "method": "GET", 1140 | "header": [ 1141 | { 1142 | "key": "Content-Type", 1143 | "value": "application/json" 1144 | }, 1145 | { 1146 | "key": "X-Requested-With", 1147 | "value": "XMLHttpRequest" 1148 | }, 1149 | { 1150 | "key": "Authorization", 1151 | "value": "Token {{token}}" 1152 | } 1153 | ], 1154 | "body": { 1155 | "mode": "raw", 1156 | "raw": "" 1157 | }, 1158 | "url": { 1159 | "raw": "{{APIURL}}/articles?favorited=jane", 1160 | "host": ["{{APIURL}}"], 1161 | "path": ["articles"], 1162 | "query": [ 1163 | { 1164 | "key": "favorited", 1165 | "value": "jane" 1166 | } 1167 | ] 1168 | } 1169 | }, 1170 | "response": [] 1171 | }, 1172 | { 1173 | "name": "Single Article by slug", 1174 | "event": [ 1175 | { 1176 | "listen": "test", 1177 | "script": { 1178 | "type": "text/javascript", 1179 | "exec": [ 1180 | "var responseJSON = JSON.parse(responseBody);", 1181 | "", 1182 | "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');", 1183 | "", 1184 | "var article = responseJSON.article || {};", 1185 | "", 1186 | "tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1187 | "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1188 | "tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1189 | "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1190 | "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1191 | "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1192 | "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1193 | "tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1194 | "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1195 | "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1196 | "tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1197 | "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1198 | "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1199 | "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1200 | "" 1201 | ] 1202 | } 1203 | } 1204 | ], 1205 | "request": { 1206 | "method": "GET", 1207 | "header": [ 1208 | { 1209 | "key": "Content-Type", 1210 | "value": "application/json" 1211 | }, 1212 | { 1213 | "key": "X-Requested-With", 1214 | "value": "XMLHttpRequest" 1215 | }, 1216 | { 1217 | "key": "Authorization", 1218 | "value": "Token {{token}}" 1219 | } 1220 | ], 1221 | "body": { 1222 | "mode": "raw", 1223 | "raw": "" 1224 | }, 1225 | "url": { 1226 | "raw": "{{APIURL}}/articles/{{slug}}", 1227 | "host": ["{{APIURL}}"], 1228 | "path": ["articles", "{{slug}}"] 1229 | } 1230 | }, 1231 | "response": [] 1232 | }, 1233 | { 1234 | "name": "Articles by Tag", 1235 | "event": [ 1236 | { 1237 | "listen": "test", 1238 | "script": { 1239 | "type": "text/javascript", 1240 | "exec": [ 1241 | "var is200Response = responseCode.code === 200;", 1242 | "", 1243 | "tests['Response code is 200 OK'] = is200Response;", 1244 | "", 1245 | "if(is200Response){", 1246 | " var responseJSON = JSON.parse(responseBody);", 1247 | "", 1248 | " tests['Response contains \"articles\" property'] = responseJSON.hasOwnProperty('articles');", 1249 | " tests['Response contains \"articlesCount\" property'] = responseJSON.hasOwnProperty('articlesCount');", 1250 | " tests['articlesCount is an integer'] = Number.isInteger(responseJSON.articlesCount);", 1251 | "", 1252 | " if(responseJSON.articles.length){", 1253 | " var article = responseJSON.articles[0];", 1254 | "", 1255 | " tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1256 | " tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1257 | " tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1258 | " tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1259 | " tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1260 | " tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1261 | " tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1262 | " tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1263 | " tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1264 | " tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1265 | " tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1266 | " tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1267 | " tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1268 | " tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1269 | " } else {", 1270 | " tests['articlesCount is 0 when feed is empty'] = responseJSON.articlesCount === 0;", 1271 | " }", 1272 | "}", 1273 | "" 1274 | ] 1275 | } 1276 | } 1277 | ], 1278 | "request": { 1279 | "method": "GET", 1280 | "header": [ 1281 | { 1282 | "key": "Content-Type", 1283 | "value": "application/json" 1284 | }, 1285 | { 1286 | "key": "X-Requested-With", 1287 | "value": "XMLHttpRequest" 1288 | }, 1289 | { 1290 | "key": "Authorization", 1291 | "value": "Token {{token}}" 1292 | } 1293 | ], 1294 | "body": { 1295 | "mode": "raw", 1296 | "raw": "" 1297 | }, 1298 | "url": { 1299 | "raw": "{{APIURL}}/articles?tag=dragons", 1300 | "host": ["{{APIURL}}"], 1301 | "path": ["articles"], 1302 | "query": [ 1303 | { 1304 | "key": "tag", 1305 | "value": "dragons" 1306 | } 1307 | ] 1308 | } 1309 | }, 1310 | "response": [] 1311 | }, 1312 | { 1313 | "name": "Update Article", 1314 | "event": [ 1315 | { 1316 | "listen": "test", 1317 | "script": { 1318 | "type": "text/javascript", 1319 | "exec": [ 1320 | "if (!(environment.isIntegrationTest)) {", 1321 | "var responseJSON = JSON.parse(responseBody);", 1322 | "", 1323 | "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');", 1324 | "", 1325 | "var article = responseJSON.article || {};", 1326 | "", 1327 | "tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1328 | "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1329 | "tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1330 | "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1331 | "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1332 | "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1333 | "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1334 | "tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1335 | "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1336 | "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1337 | "tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1338 | "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1339 | "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1340 | "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1341 | "}", 1342 | "" 1343 | ] 1344 | } 1345 | } 1346 | ], 1347 | "request": { 1348 | "method": "PUT", 1349 | "header": [ 1350 | { 1351 | "key": "Content-Type", 1352 | "value": "application/json" 1353 | }, 1354 | { 1355 | "key": "X-Requested-With", 1356 | "value": "XMLHttpRequest" 1357 | }, 1358 | { 1359 | "key": "Authorization", 1360 | "value": "Token {{token}}" 1361 | } 1362 | ], 1363 | "body": { 1364 | "mode": "raw", 1365 | "raw": "{\"article\":{\"body\":\"With two hands\"}}" 1366 | }, 1367 | "url": { 1368 | "raw": "{{APIURL}}/articles/{{slug}}", 1369 | "host": ["{{APIURL}}"], 1370 | "path": ["articles", "{{slug}}"] 1371 | } 1372 | }, 1373 | "response": [] 1374 | }, 1375 | { 1376 | "name": "Favorite Article", 1377 | "event": [ 1378 | { 1379 | "listen": "test", 1380 | "script": { 1381 | "type": "text/javascript", 1382 | "exec": [ 1383 | "var responseJSON = JSON.parse(responseBody);", 1384 | "", 1385 | "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');", 1386 | "", 1387 | "var article = responseJSON.article || {};", 1388 | "", 1389 | "tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1390 | "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1391 | "tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1392 | "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1393 | "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1394 | "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1395 | "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1396 | "tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1397 | "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1398 | "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1399 | "tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1400 | "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1401 | "tests[\"Article's 'favorited' property is true\"] = article.favorited === true;", 1402 | "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1403 | "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1404 | "tests[\"Article's 'favoritesCount' property is greater than 0\"] = article.favoritesCount > 0;", 1405 | "" 1406 | ] 1407 | } 1408 | } 1409 | ], 1410 | "request": { 1411 | "method": "POST", 1412 | "header": [ 1413 | { 1414 | "key": "Content-Type", 1415 | "value": "application/json" 1416 | }, 1417 | { 1418 | "key": "X-Requested-With", 1419 | "value": "XMLHttpRequest" 1420 | }, 1421 | { 1422 | "key": "Authorization", 1423 | "value": "Token {{token}}" 1424 | } 1425 | ], 1426 | "body": { 1427 | "mode": "raw", 1428 | "raw": "" 1429 | }, 1430 | "url": { 1431 | "raw": "{{APIURL}}/articles/{{slug}}/favorite", 1432 | "host": ["{{APIURL}}"], 1433 | "path": ["articles", "{{slug}}", "favorite"] 1434 | } 1435 | }, 1436 | "response": [] 1437 | }, 1438 | { 1439 | "name": "Unfavorite Article", 1440 | "event": [ 1441 | { 1442 | "listen": "test", 1443 | "script": { 1444 | "type": "text/javascript", 1445 | "exec": [ 1446 | "var responseJSON = JSON.parse(responseBody);", 1447 | "", 1448 | "tests['Response contains \"article\" property'] = responseJSON.hasOwnProperty('article');", 1449 | "", 1450 | "var article = responseJSON.article || {};", 1451 | "", 1452 | "tests['Article has \"title\" property'] = article.hasOwnProperty('title');", 1453 | "tests['Article has \"slug\" property'] = article.hasOwnProperty('slug');", 1454 | "tests['Article has \"body\" property'] = article.hasOwnProperty('body');", 1455 | "tests['Article has \"createdAt\" property'] = article.hasOwnProperty('createdAt');", 1456 | "tests['Article\\'s \"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.createdAt);", 1457 | "tests['Article has \"updatedAt\" property'] = article.hasOwnProperty('updatedAt');", 1458 | "tests['Article\\'s \"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(article.updatedAt);", 1459 | "tests['Article has \"description\" property'] = article.hasOwnProperty('description');", 1460 | "tests['Article has \"tagList\" property'] = article.hasOwnProperty('tagList');", 1461 | "tests['Article\\'s \"tagList\" property is an Array'] = Array.isArray(article.tagList);", 1462 | "tests['Article has \"author\" property'] = article.hasOwnProperty('author');", 1463 | "tests['Article has \"favorited\" property'] = article.hasOwnProperty('favorited');", 1464 | "tests['Article has \"favoritesCount\" property'] = article.hasOwnProperty('favoritesCount');", 1465 | "tests['favoritesCount is an integer'] = Number.isInteger(article.favoritesCount);", 1466 | "tests[\"Article's \\\"favorited\\\" property is false\"] = article.favorited === false;", 1467 | "" 1468 | ] 1469 | } 1470 | } 1471 | ], 1472 | "request": { 1473 | "method": "DELETE", 1474 | "header": [ 1475 | { 1476 | "key": "Content-Type", 1477 | "value": "application/json" 1478 | }, 1479 | { 1480 | "key": "X-Requested-With", 1481 | "value": "XMLHttpRequest" 1482 | }, 1483 | { 1484 | "key": "Authorization", 1485 | "value": "Token {{token}}" 1486 | } 1487 | ], 1488 | "body": { 1489 | "mode": "raw", 1490 | "raw": "" 1491 | }, 1492 | "url": { 1493 | "raw": "{{APIURL}}/articles/{{slug}}/favorite", 1494 | "host": ["{{APIURL}}"], 1495 | "path": ["articles", "{{slug}}", "favorite"] 1496 | } 1497 | }, 1498 | "response": [] 1499 | }, 1500 | { 1501 | "name": "Create Comment for Article", 1502 | "event": [ 1503 | { 1504 | "listen": "test", 1505 | "script": { 1506 | "id": "9f90c364-cc68-4728-961a-85eb00197d7b", 1507 | "type": "text/javascript", 1508 | "exec": [ 1509 | "var responseJSON = JSON.parse(responseBody);", 1510 | "", 1511 | "tests['Response contains \"comment\" property'] = responseJSON.hasOwnProperty('comment');", 1512 | "", 1513 | "var comment = responseJSON.comment || {};", 1514 | "", 1515 | "tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", 1516 | "pm.globals.set('commentId', comment.id);", 1517 | "", 1518 | "tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", 1519 | "tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", 1520 | "tests['\"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(comment.createdAt);", 1521 | "tests['Comment has \"updatedAt\" property'] = comment.hasOwnProperty('updatedAt');", 1522 | "tests['\"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(comment.updatedAt);", 1523 | "tests['Comment has \"author\" property'] = comment.hasOwnProperty('author');", 1524 | "" 1525 | ] 1526 | } 1527 | } 1528 | ], 1529 | "request": { 1530 | "method": "POST", 1531 | "header": [ 1532 | { 1533 | "key": "Content-Type", 1534 | "value": "application/json" 1535 | }, 1536 | { 1537 | "key": "X-Requested-With", 1538 | "value": "XMLHttpRequest" 1539 | }, 1540 | { 1541 | "key": "Authorization", 1542 | "value": "Token {{token}}" 1543 | } 1544 | ], 1545 | "body": { 1546 | "mode": "raw", 1547 | "raw": "{\"comment\":{\"body\":\"Thank you so much!\"}}" 1548 | }, 1549 | "url": { 1550 | "raw": "{{APIURL}}/articles/{{slug}}/comments", 1551 | "host": ["{{APIURL}}"], 1552 | "path": ["articles", "{{slug}}", "comments"] 1553 | } 1554 | }, 1555 | "response": [] 1556 | }, 1557 | { 1558 | "name": "All Comments for Article", 1559 | "event": [ 1560 | { 1561 | "listen": "test", 1562 | "script": { 1563 | "type": "text/javascript", 1564 | "exec": [ 1565 | "var is200Response = responseCode.code === 200", 1566 | "", 1567 | "tests['Response code is 200 OK'] = is200Response;", 1568 | "", 1569 | "if(is200Response){", 1570 | " var responseJSON = JSON.parse(responseBody);", 1571 | "", 1572 | " tests['Response contains \"comments\" property'] = responseJSON.hasOwnProperty('comments');", 1573 | "", 1574 | " if(responseJSON.comments.length){", 1575 | " var comment = responseJSON.comments[0];", 1576 | "", 1577 | " tests['Comment has \"id\" property'] = comment.hasOwnProperty('id');", 1578 | " tests['Comment has \"body\" property'] = comment.hasOwnProperty('body');", 1579 | " tests['Comment has \"createdAt\" property'] = comment.hasOwnProperty('createdAt');", 1580 | " tests['\"createdAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(comment.createdAt);", 1581 | " tests['Comment has \"updatedAt\" property'] = comment.hasOwnProperty('updatedAt');", 1582 | " tests['\"updatedAt\" property is an ISO 8601 timestamp'] = /^\\d{4,}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d.\\d+(?:[+-][0-2]\\d:[0-5]\\d|Z)$/.test(comment.updatedAt);", 1583 | " tests['Comment has \"author\" property'] = comment.hasOwnProperty('author');", 1584 | " }", 1585 | "}", 1586 | "" 1587 | ] 1588 | } 1589 | } 1590 | ], 1591 | "request": { 1592 | "method": "GET", 1593 | "header": [ 1594 | { 1595 | "key": "Content-Type", 1596 | "value": "application/json" 1597 | }, 1598 | { 1599 | "key": "X-Requested-With", 1600 | "value": "XMLHttpRequest" 1601 | }, 1602 | { 1603 | "key": "Authorization", 1604 | "value": "Token {{token}}" 1605 | } 1606 | ], 1607 | "body": { 1608 | "mode": "raw", 1609 | "raw": "" 1610 | }, 1611 | "url": { 1612 | "raw": "{{APIURL}}/articles/{{slug}}/comments", 1613 | "host": ["{{APIURL}}"], 1614 | "path": ["articles", "{{slug}}", "comments"] 1615 | } 1616 | }, 1617 | "response": [] 1618 | }, 1619 | { 1620 | "name": "Delete Comment for Article", 1621 | "request": { 1622 | "method": "DELETE", 1623 | "header": [ 1624 | { 1625 | "key": "Content-Type", 1626 | "value": "application/json" 1627 | }, 1628 | { 1629 | "key": "X-Requested-With", 1630 | "value": "XMLHttpRequest" 1631 | }, 1632 | { 1633 | "key": "Authorization", 1634 | "value": "Token {{token}}" 1635 | } 1636 | ], 1637 | "body": { 1638 | "mode": "raw", 1639 | "raw": "" 1640 | }, 1641 | "url": { 1642 | "raw": "{{APIURL}}/articles/{{slug}}/comments/{{commentId}}", 1643 | "host": ["{{APIURL}}"], 1644 | "path": ["articles", "{{slug}}", "comments", "{{commentId}}"] 1645 | } 1646 | }, 1647 | "response": [] 1648 | }, 1649 | { 1650 | "name": "Delete Article", 1651 | "request": { 1652 | "method": "DELETE", 1653 | "header": [ 1654 | { 1655 | "key": "Content-Type", 1656 | "value": "application/json" 1657 | }, 1658 | { 1659 | "key": "X-Requested-With", 1660 | "value": "XMLHttpRequest" 1661 | }, 1662 | { 1663 | "key": "Authorization", 1664 | "value": "Token {{token}}" 1665 | } 1666 | ], 1667 | "body": { 1668 | "mode": "raw", 1669 | "raw": "" 1670 | }, 1671 | "url": { 1672 | "raw": "{{APIURL}}/articles/{{slug}}", 1673 | "host": ["{{APIURL}}"], 1674 | "path": ["articles", "{{slug}}"] 1675 | } 1676 | }, 1677 | "response": [] 1678 | } 1679 | ], 1680 | "event": [ 1681 | { 1682 | "listen": "prerequest", 1683 | "script": { 1684 | "id": "67853a4a-e972-4573-a295-dad12a46a9d7", 1685 | "type": "text/javascript", 1686 | "exec": [""] 1687 | } 1688 | }, 1689 | { 1690 | "listen": "test", 1691 | "script": { 1692 | "id": "3057f989-15e4-484e-b8fa-a041043d0ac0", 1693 | "type": "text/javascript", 1694 | "exec": [""] 1695 | } 1696 | } 1697 | ] 1698 | }, 1699 | { 1700 | "name": "Profiles", 1701 | "item": [ 1702 | { 1703 | "name": "Register Celeb", 1704 | "event": [ 1705 | { 1706 | "listen": "test", 1707 | "script": { 1708 | "type": "text/javascript", 1709 | "exec": [ 1710 | "if (!(environment.isIntegrationTest)) {", 1711 | "var responseJSON = JSON.parse(responseBody);", 1712 | "", 1713 | "tests['Response contains \"user\" property'] = responseJSON.hasOwnProperty('user');", 1714 | "", 1715 | "var user = responseJSON.user || {};", 1716 | "", 1717 | "tests['User has \"email\" property'] = user.hasOwnProperty('email');", 1718 | "tests['User has \"username\" property'] = user.hasOwnProperty('username');", 1719 | "tests['User has \"bio\" property'] = user.hasOwnProperty('bio');", 1720 | "tests['User has \"image\" property'] = user.hasOwnProperty('image');", 1721 | "tests['User has \"token\" property'] = user.hasOwnProperty('token');", 1722 | "}", 1723 | "" 1724 | ] 1725 | } 1726 | } 1727 | ], 1728 | "request": { 1729 | "method": "POST", 1730 | "header": [ 1731 | { 1732 | "key": "Content-Type", 1733 | "value": "application/json" 1734 | }, 1735 | { 1736 | "key": "X-Requested-With", 1737 | "value": "XMLHttpRequest" 1738 | } 1739 | ], 1740 | "body": { 1741 | "mode": "raw", 1742 | "raw": "{\"user\":{\"email\":\"celeb_{{EMAIL}}\", \"password\":\"{{PASSWORD}}\", \"username\":\"celeb_{{USERNAME}}\"}}" 1743 | }, 1744 | "url": { 1745 | "raw": "{{APIURL}}/users", 1746 | "host": ["{{APIURL}}"], 1747 | "path": ["users"] 1748 | } 1749 | }, 1750 | "response": [] 1751 | }, 1752 | { 1753 | "name": "Profile", 1754 | "event": [ 1755 | { 1756 | "listen": "test", 1757 | "script": { 1758 | "type": "text/javascript", 1759 | "exec": [ 1760 | "if (!(environment.isIntegrationTest)) {", 1761 | "var is200Response = responseCode.code === 200;", 1762 | "", 1763 | "tests['Response code is 200 OK'] = is200Response;", 1764 | "", 1765 | "if(is200Response){", 1766 | " var responseJSON = JSON.parse(responseBody);", 1767 | "", 1768 | " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", 1769 | " ", 1770 | " var profile = responseJSON.profile || {};", 1771 | " ", 1772 | " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", 1773 | " tests['Profile has \"bio\" property'] = profile.hasOwnProperty('bio');", 1774 | " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", 1775 | " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", 1776 | "}", 1777 | "}", 1778 | "" 1779 | ] 1780 | } 1781 | } 1782 | ], 1783 | "request": { 1784 | "method": "GET", 1785 | "header": [ 1786 | { 1787 | "key": "Content-Type", 1788 | "value": "application/json" 1789 | }, 1790 | { 1791 | "key": "X-Requested-With", 1792 | "value": "XMLHttpRequest" 1793 | }, 1794 | { 1795 | "key": "Authorization", 1796 | "value": "Token {{token}}" 1797 | } 1798 | ], 1799 | "body": { 1800 | "mode": "raw", 1801 | "raw": "" 1802 | }, 1803 | "url": { 1804 | "raw": "{{APIURL}}/profiles/celeb_{{USERNAME}}", 1805 | "host": ["{{APIURL}}"], 1806 | "path": ["profiles", "celeb_{{USERNAME}}"] 1807 | } 1808 | }, 1809 | "response": [] 1810 | }, 1811 | { 1812 | "name": "Follow Profile", 1813 | "event": [ 1814 | { 1815 | "listen": "test", 1816 | "script": { 1817 | "type": "text/javascript", 1818 | "exec": [ 1819 | "if (!(environment.isIntegrationTest)) {", 1820 | "var is200Response = responseCode.code === 200;", 1821 | "", 1822 | "tests['Response code is 200 OK'] = is200Response;", 1823 | "", 1824 | "if(is200Response){", 1825 | " var responseJSON = JSON.parse(responseBody);", 1826 | "", 1827 | " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", 1828 | " ", 1829 | " var profile = responseJSON.profile || {};", 1830 | " ", 1831 | " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", 1832 | " tests['Profile has \"bio\" property'] = profile.hasOwnProperty('bio');", 1833 | " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", 1834 | " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", 1835 | " tests['Profile\\'s \"following\" property is true'] = profile.following === true;", 1836 | "}", 1837 | "}", 1838 | "" 1839 | ] 1840 | } 1841 | } 1842 | ], 1843 | "request": { 1844 | "method": "POST", 1845 | "header": [ 1846 | { 1847 | "key": "Content-Type", 1848 | "value": "application/json" 1849 | }, 1850 | { 1851 | "key": "X-Requested-With", 1852 | "value": "XMLHttpRequest" 1853 | }, 1854 | { 1855 | "key": "Authorization", 1856 | "value": "Token {{token}}" 1857 | } 1858 | ], 1859 | "body": { 1860 | "mode": "raw", 1861 | "raw": "{\"user\":{\"email\":\"{{EMAIL}}\"}}" 1862 | }, 1863 | "url": { 1864 | "raw": "{{APIURL}}/profiles/celeb_{{USERNAME}}/follow", 1865 | "host": ["{{APIURL}}"], 1866 | "path": ["profiles", "celeb_{{USERNAME}}", "follow"] 1867 | } 1868 | }, 1869 | "response": [] 1870 | }, 1871 | { 1872 | "name": "Unfollow Profile", 1873 | "event": [ 1874 | { 1875 | "listen": "test", 1876 | "script": { 1877 | "type": "text/javascript", 1878 | "exec": [ 1879 | "if (!(environment.isIntegrationTest)) {", 1880 | "var is200Response = responseCode.code === 200;", 1881 | "", 1882 | "tests['Response code is 200 OK'] = is200Response;", 1883 | "", 1884 | "if(is200Response){", 1885 | " var responseJSON = JSON.parse(responseBody);", 1886 | "", 1887 | " tests['Response contains \"profile\" property'] = responseJSON.hasOwnProperty('profile');", 1888 | " ", 1889 | " var profile = responseJSON.profile || {};", 1890 | " ", 1891 | " tests['Profile has \"username\" property'] = profile.hasOwnProperty('username');", 1892 | " tests['Profile has \"bio\" property'] = profile.hasOwnProperty('bio');", 1893 | " tests['Profile has \"image\" property'] = profile.hasOwnProperty('image');", 1894 | " tests['Profile has \"following\" property'] = profile.hasOwnProperty('following');", 1895 | " tests['Profile\\'s \"following\" property is false'] = profile.following === false;", 1896 | "}", 1897 | "}", 1898 | "" 1899 | ] 1900 | } 1901 | } 1902 | ], 1903 | "request": { 1904 | "method": "DELETE", 1905 | "header": [ 1906 | { 1907 | "key": "Content-Type", 1908 | "value": "application/json" 1909 | }, 1910 | { 1911 | "key": "X-Requested-With", 1912 | "value": "XMLHttpRequest" 1913 | }, 1914 | { 1915 | "key": "Authorization", 1916 | "value": "Token {{token}}" 1917 | } 1918 | ], 1919 | "body": { 1920 | "mode": "raw", 1921 | "raw": "" 1922 | }, 1923 | "url": { 1924 | "raw": "{{APIURL}}/profiles/celeb_{{USERNAME}}/follow", 1925 | "host": ["{{APIURL}}"], 1926 | "path": ["profiles", "celeb_{{USERNAME}}", "follow"] 1927 | } 1928 | }, 1929 | "response": [] 1930 | } 1931 | ] 1932 | }, 1933 | { 1934 | "name": "Tags", 1935 | "item": [ 1936 | { 1937 | "name": "All Tags", 1938 | "event": [ 1939 | { 1940 | "listen": "test", 1941 | "script": { 1942 | "type": "text/javascript", 1943 | "exec": [ 1944 | "var is200Response = responseCode.code === 200;", 1945 | "", 1946 | "tests['Response code is 200 OK'] = is200Response;", 1947 | "", 1948 | "if(is200Response){", 1949 | " var responseJSON = JSON.parse(responseBody);", 1950 | " ", 1951 | " tests['Response contains \"tags\" property'] = responseJSON.hasOwnProperty('tags');", 1952 | " tests['\"tags\" property returned as array'] = Array.isArray(responseJSON.tags);", 1953 | "}", 1954 | "" 1955 | ] 1956 | } 1957 | } 1958 | ], 1959 | "request": { 1960 | "method": "GET", 1961 | "header": [ 1962 | { 1963 | "key": "Content-Type", 1964 | "value": "application/json" 1965 | }, 1966 | { 1967 | "key": "X-Requested-With", 1968 | "value": "XMLHttpRequest" 1969 | } 1970 | ], 1971 | "body": { 1972 | "mode": "raw", 1973 | "raw": "" 1974 | }, 1975 | "url": { 1976 | "raw": "{{APIURL}}/tags", 1977 | "host": ["{{APIURL}}"], 1978 | "path": ["tags"] 1979 | } 1980 | }, 1981 | "response": [] 1982 | } 1983 | ] 1984 | } 1985 | ] 1986 | } 1987 | --------------------------------------------------------------------------------