Skip to content

Commit

Permalink
[WIP] Apps CRUD.
Browse files Browse the repository at this point in the history
  • Loading branch information
cscatolini committed Nov 8, 2016
1 parent dd07fe6 commit e5c9845
Show file tree
Hide file tree
Showing 10 changed files with 699 additions and 70 deletions.
482 changes: 429 additions & 53 deletions lib/marathon.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/marathon.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions migrations/20161103221706-creating app model.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
up: (queryInterface, Sequelize) => {
const App = queryInterface.createTable('app', {
const App = queryInterface.createTable('apps', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
Expand Down Expand Up @@ -37,5 +37,5 @@ module.exports = {
},

down: queryInterface =>
queryInterface.dropTable('app'),
queryInterface.dropTable('apps'),
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,19 @@
"dependencies": {
"babel-runtime": "^6.11.6",
"bluebird": "^3.4.6",
"boom": "^4.2.0",
"bufferutil": "^1.2.1",
"bunyan": "^1.8.1",
"commander": "^2.9.0",
"config": "^1.21.0",
"humps": "^2.0.0",
"js-yaml": "^3.6.1",
"json-loader": "^0.5.4",
"kafka-node": "^1.0.5",
"koa": "^2.0.0-alpha.7",
"koa-route": "^3.2.0",
"koa-bodyparser": "^3.2.0",
"koa-router": "^7.0.1",
"koa-validate": "^1.0.7",
"mongoose": "^4.6.5",
"pg": "^6.1.0",
"pg-hstore": "^2.3.2",
Expand Down
29 changes: 22 additions & 7 deletions src/api/app.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { camelize } from 'humps'
import koaBodyparser from 'koa-bodyparser'
import koaRouter from 'koa-router'
import koaValidate from 'koa-validate'
import path from 'path'
import Koa from 'koa'
import _ from 'koa-route'
import Logger from '../extensions/logger'
import { AppHandler, AppsHandler } from './handlers/app'
import HealthcheckHandler from './handlers/healthcheck'
import { connect as redisConnect } from '../extensions/redis'
import { connect as pgConnect } from '../extensions/postgresql'
Expand Down Expand Up @@ -29,6 +33,8 @@ export default class MarathonApp {

// Include handlers here
handlers.push(new HealthcheckHandler(self))
handlers.push(new AppsHandler(self))
handlers.push(new AppHandler(self))

return handlers
}
Expand Down Expand Up @@ -109,30 +115,39 @@ export default class MarathonApp {
}

configureMiddleware() {
this.koaApp.use(koaBodyparser())
this.koaApp.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
ctx.set('X-Response-Time', `${ms}ms`)
})
koaValidate(this.koaApp)
}

async initializeApp() {
await this.initializeServices()
const router = koaRouter()
this.handlers.forEach((handler) => {
this.allowedMethods.forEach((methodName) => {
if (!handler[methodName]) {
return
}
const handlerMethod = handler[methodName].bind(handler)
const method = _[methodName]
this.koaApp.use(
method(handler.route, async (ctx) => {
await handlerMethod(ctx)
})
)
const method = router[methodName]
const args = [handler.route]
const validateName = camelize(`validate_${methodName}`)
if (handler[validateName]) {
args.push(handler[validateName].bind(handler))
}
args.push(async (ctx) => {
await handlerMethod.apply(handler, [ctx])
})
method.apply(router, args)
})
})
this.koaApp.use(router.routes())
this.koaApp.use(router.allowedMethods())
}

async run() {
Expand Down
90 changes: 90 additions & 0 deletions src/api/handlers/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
const Boom = require('boom')

export class AppsHandler {
constructor(app) {
this.app = app
this.route = '/apps'
}

async validatePost(ctx, next) {
ctx.checkHeader('user-email').notEmpty().isEmail()
ctx.checkBody('bundleId').notEmpty().match(/^[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)+$/i)
ctx.checkBody('key').notEmpty().len(1, 255)
if (ctx.errors) {
const error = Boom.badData('wrong arguments', ctx.errors)
ctx.status = 422
ctx.body = error.output.payload
ctx.body.data = error.data
return
}
await next()
}

async get(ctx) {
const apps = await this.app.db.App.findAll()
ctx.body = { apps }
ctx.status = 200
}

async post(ctx) {
const body = ctx.request.body
body.createdBy = ctx.request.header['user-email']
const app = await this.app.db.App.create(body)
ctx.body = { app }
ctx.status = 201
}
}

export class AppHandler {
constructor(app) {
this.app = app
this.route = '/apps/:id'
}

async validatePut(ctx, next) {
ctx.checkBody('bundleId').notEmpty().match(/^[a-z0-9]+\.[a-z0-9]+(\.[a-z0-9]+)+$/i)
ctx.checkBody('key').notEmpty().len(1, 255)
if (ctx.errors) {
const error = Boom.badData('wrong arguments', ctx.errors)
ctx.status = 422
ctx.body = error.output.payload
ctx.body.data = error.data
return
}
await next()
}

async get(ctx) {
const app = await this.app.db.App.findById(this.params.id)
if (!app) {
ctx.status = 404
return
}
ctx.body = { app }
ctx.status = 200
}

async put(ctx) {
const body = ctx.request.body
const app = await this.app.db.App.findById(this.params.id)
if (!app) {
ctx.status = 404
return
}

const updatedApp = await app.updateAttributes(body)
ctx.body = { app: updatedApp }
ctx.status = 200
}

async delete(ctx) {
const app = await this.app.db.App.findById(this.params.id)
if (!app) {
ctx.status = 404
return
}

await app.destroy()
ctx.status = 204
}
}
6 changes: 3 additions & 3 deletions src/extensions/postgresql.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ export async function connect(pgUrl, options, logger) {
}

logr.debug('Loading models...')
models.forEach((model) => {
db[model.name] = model
logr.debug({ model: model.name }, 'Model loaded successfully.')
Object.keys(models).forEach((model) => {
db[model] = models[model](client, Sequelize.DataTypes)
logr.debug({ model }, 'Model loaded successfully.')
})

logr.debug('All models loaded successfully.')
Expand Down
4 changes: 2 additions & 2 deletions src/models/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import App from './app'

const models = [
const models = {
App,
]
}
export default models
145 changes: 145 additions & 0 deletions test/unit/api/handlers/appTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { expect } from '../../common'
import uuid from 'uuid'

describe('Handlers', () => {
describe('Apps Handler', () => {
describe('GET', () => {
it('should return 200 and an empty list of apps if there are no apps', async function () {
await this.app.db.App.destroy({ truncate: true })
const res = await this.request.get('/apps')
expect(res.status).to.equal(200)

const body = res.body
expect(body).to.be.an('object')

expect(body.apps).to.exist()
expect(body.apps).to.have.length(0)
})

it('should return 200 and a list of apps', async function () {
const app = {
key: uuid.v4(),
bundleId: 'com.app.my',
createdBy: 'someone@somewhere.com',
}
await this.app.db.App.create(app)
const res = await this.request.get('/apps')
expect(res.status).to.equal(200)

const body = res.body
expect(body).to.be.an('object')

expect(body.apps).to.exist()
expect(body.apps).to.have.length.at.least(1)

const myApp = body.apps.filter(a => a.key === app.key)[0]
expect(myApp).to.exist()
expect(myApp.key).to.equal(app.key)
expect(myApp.bundleId).to.equal(app.bundleId)
expect(myApp.createdBy).to.equal(app.createdBy)
})
})

describe('POST', () => {
let app
let userEmail

beforeEach(() => {
app = {
key: uuid.v4(),
bundleId: 'com.app.my',
}
userEmail = 'someone@somewhere.com'
})

it('should return 201 and the created app', async function () {
const res = await this.request.post('/apps').send(app).set('user-email', userEmail)
expect(res.status).to.equal(201)

const body = res.body
expect(body).to.be.an('object')

expect(body.app).to.exist()
expect(body.app).to.be.an('object')

expect(body.app.key).to.equal(app.key)
expect(body.app.bundleId).to.equal(app.bundleId)
expect(body.app.createdBy).to.equal(userEmail)
})

describe('Should fail if missing', () => {
it('user-email header', async function () {
const res = await this.request.post('/apps').send(app)
expect(res.status).to.equal(422)

const body = res.body
expect(body).to.be.an('object')

expect(body.data).to.exist()
expect(body.data).to.have.length(1)
expect(body.data[0]).to.have.property('user-email')
expect(body.data[0]['user-email']).to.contain('empty')
})

const tests = [
{ args: 'key' },
{ args: 'bundleId' },
]

tests.forEach((test) => {
it(test.args, async function () {
delete app[test.args]
const res = await this.request.post('/apps').send(app).set('user-email', userEmail)
expect(res.status).to.equal(422)

const body = res.body
expect(body).to.be.an('object')

expect(body.data).to.exist()
expect(body.data).to.have.length(1)
expect(body.data[0]).to.have.property(test.args)
expect(body.data[0][test.args]).to.contain('empty')
})
})
})

describe('Should fail if invalid', () => {
it('user-email header', async function () {
const res = await this.request.post('/apps').send(app).set('user-email', 'not an email')
expect(res.status).to.equal(422)

const body = res.body
expect(body).to.be.an('object')

expect(body.data).to.exist()
expect(body.data).to.have.length(1)
expect(body.data[0]).to.have.property('user-email')
expect(body.data[0]['user-email']).to.contain('email format')
})

const tests = [
{ args: 'key', invalidParam: '', reason: 'empty' },
// { args: 'key', invalidParam: 'a'.repeat(256), reason: 'too long' },
{ args: 'bundleId', invalidParam: '', reason: 'empty' },
{ args: 'bundleId', invalidParam: 'a.s', reason: 'bad format.' },
]

tests.forEach((test) => {
it(test.args, async function () {
app[test.args] = test.invalidParam
const res = await this.request.post('/apps').send(app).set('user-email', userEmail)
expect(res.status).to.equal(422)

const body = res.body
expect(body).to.be.an('object')

expect(body.data).to.exist()
expect(body.data).to.have.length(1)
expect(body.data[0]).to.have.property(test.args)
expect(body.data[0][test.args]).to.contain(test.reason)
})
})
})
})
})
})
1 change: 0 additions & 1 deletion test/unit/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ beforeEach(async function () {
config.app.port = PORT
if (!app) {
app = new MarathonApp(config)
await app.initializeApp()
}
this.app = app
this.wssPort = PORT
Expand Down

0 comments on commit e5c9845

Please sign in to comment.