diff --git a/.gitignore b/.gitignore index 1cd4f51..76abd7f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ web/css/ # vendor files web/vendor/ + +# coverage reports +coverage/ diff --git a/.openshift/action_hooks/pre_start b/.openshift/action_hooks/pre_start deleted file mode 100755 index 4103665..0000000 --- a/.openshift/action_hooks/pre_start +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -# this starts the logentries agent -${OPENSHIFT_DATA_DIR}/start-le diff --git a/.travis.yml b/.travis.yml index 2cc2e17..d33af66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: node_js node_js: - '6' +after_script: npm run test-coverage && cat ./coverage/lcov.info | coveralls deploy: provider: openshift user: mugo@forfuture.co.ke diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df7de7..a5b03b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ This project adheres to [Semantic Versioning](http://semver.org/). +## [0.7.0][0.7.0] - 2016-11-16 + +Changed: + +* `/api/networks/:network` returns content of data file under `network` key + + ## [0.6.0][0.6.0] - 2016-11-14 Added: @@ -92,4 +99,5 @@ This is the very first version. [0.4.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.4.0 [0.5.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.5.0 [0.6.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.6.0 -[Unreleased]: https://github.com/forfuturellc/mmtc-ke/compare/v0.6.0...HEAD +[0.7.0]:https://github.com/forfuturellc/mmtc-ke/releases/tag/v0.7.0 +[Unreleased]: https://github.com/forfuturellc/mmtc-ke/compare/v0.7.0...HEAD diff --git a/Gruntfile.js b/Gruntfile.js index 4192264..7c5c54c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -13,6 +13,12 @@ exports = module.exports = (grunt) => { 'Gruntfile.js', 'routes/**/*.js', 'web/js/*.js', + 'test/**/*.js', + ], + }, + mochaTest: { + test: [ + 'test/test.*.js', ], }, sass: { @@ -30,5 +36,5 @@ exports = module.exports = (grunt) => { grunt.registerTask('build', ['sass']); grunt.registerTask('lint', ['eslint']); - grunt.registerTask('test', ['lint']); + grunt.registerTask('test', ['lint', 'mochaTest']); }; diff --git a/README.md b/README.md index 4e4f20a..f762e3b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Supported Node.js Versions](https://img.shields.io/badge/node->=6-green.svg)](https://github.com/forfuturellc/mmtc-ke) [![Build Status](https://travis-ci.org/forfuturellc/mmtc-ke.svg?branch=master)](https://travis-ci.org/forfuturellc/mmtc-ke) - + [![Coverage Status](https://coveralls.io/repos/github/forfuturellc/mmtc-ke/badge.svg?branch=master)](https://coveralls.io/github/forfuturellc/mmtc-ke?branch=master) [![Dependency Status](https://gemnasium.com/forfuturellc/mmtc-ke.svg)](https://gemnasium.com/forfuturellc/mmtc-ke) diff --git a/app.js b/app.js index 6addb5d..6c849b3 100644 --- a/app.js +++ b/app.js @@ -7,6 +7,11 @@ */ +exports = module.exports = { + run, +}; + + // built-in modules const path = require('path'); @@ -94,8 +99,25 @@ app.use(function(err, req, res, next) { // eslint-disable-line no-unused-vars }); -debug('starting server'); -app.listen(config.get('server.port'), config.get('server.ip'), function() { - logger.info('server listening'); - debug('server started at http://%s:%s', config.get('server.ip'), config.get('server.port')); -}); +function run(options, done) { + options = options || {}; + if (!options.host) { + options.host = config.get('server.ip'); + } + if (!options.port) { + options.port = config.get('server.port'); + } + + debug('starting server'); + app.listen(options.port, options.host, function() { + logger.info('server listening'); + debug('server started at http://%s:%s', options.host, options.port); + if (done) return done(); + }); +} + + +if (require.main === module) { + debug('running as script'); + run(); +} diff --git a/data/SPEC.md b/data/SPEC.md index 3ec579a..355a813 100644 --- a/data/SPEC.md +++ b/data/SPEC.md @@ -82,8 +82,6 @@ additions: this transaction can **not** be determined using our data (depends on external factors, e.g. merchant reputation) -Therefore, the cost is accurate to **1 KES**. - ### USSDCode diff --git a/docs/api.md b/docs/api.md index 7ccfd59..d59c643 100644 --- a/docs/api.md +++ b/docs/api.md @@ -17,6 +17,9 @@ and `https://mmtcke-forfutureco.rhcloud.com/api` API Characteristics: + * **Beta**: we are working on this API. Please watch the + [Github repository][repo] for updates. Some of these updates may break + the API, until we declare the API **stable** * **JSON**: data back-and-forth is formatted in JSON * **Status codes**: responses are sent back with sensible status codes * **Unauthenticated**: no authentication token is required, currently @@ -77,6 +80,18 @@ GET /networks/:network Retrieve data for `network`. This basically returns the content of the data file for `network`. +Example partial response: + +```http +200 OK +``` + +```json +{ + "network": { /* CONTENT of the data file */ } +} +``` + --- # @@ -108,3 +123,4 @@ Example response: [data-files]:https://github.com/forfuturellc/mmtc-ke/tree/master/data +[repo]:https://github.com/forfuturellc/mmtc-ke diff --git a/docs/news.md b/docs/news.md index 89962d7..9029fc2 100644 --- a/docs/news.md +++ b/docs/news.md @@ -1,3 +1,15 @@ +

API Change 2016-11-16

+ +There has been change in our API at +[endpoint `/networks/:network`][endpoint]. This change breaks the +API. Update your software to reflect this. + +Please note that we are not going through the process of +deprecation. The API is still in beta. + +[endpoint]:http://mmtc.forfuture.co.ke/api/#get-networks-network + +

Updated Mpesa data 2016-11-11

The [recent change in M-pesa tariffs][change-in-tariffs] has necessitated diff --git a/package.json b/package.json index c76c5c6..4ee0f7a 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "mmtc-ke", - "version": "0.6.0", + "version": "0.7.0", "private": true, "scripts": { "build": "grunt build", "postinstall": "HOME=${BOWER_HOME:-${HOME}} bower install", "start": "forever app.js", "start-dev": "DEBUG=mmtc-ke:* nodemon app.js", - "test": "grunt test" + "test": "grunt test", + "test-coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.*.js" }, "dependencies": { "body-parser": "^1.15.2", @@ -35,11 +36,17 @@ "node": ">=6" }, "devDependencies": { + "coveralls": "^2.11.15", + "elbow": "^1.0.0", "grunt": "^1.0.1", "grunt-cli": "^1.2.0", "grunt-eslint": "^19.0.0", + "grunt-mocha-test": "^0.13.2", "grunt-sass": "^1.2.1", + "istanbul": "^0.4.5", "load-grunt-tasks": "^3.5.2", + "mocha": "^3.1.2", + "mocha-lcov-reporter": "^1.2.0", "nodemon": "^1.11.0" } } diff --git a/routes/api.js b/routes/api.js index a0f8513..720c9da 100644 --- a/routes/api.js +++ b/routes/api.js @@ -54,7 +54,7 @@ router.get('/networks/:network', function(req, res, next) { networkNotFoundError.statusCode = 404; return next(networkNotFoundError); } - return res.json(network); + return res.json({ network }); }); diff --git a/schema/definitions.json b/schema/definitions.json new file mode 100644 index 0000000..bef754b --- /dev/null +++ b/schema/definitions.json @@ -0,0 +1,137 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "network": { + "type": "object", + "properties": { + "name": { + "$ref": "#/name" + }, + "meta": { + "$ref": "#/metadata" + }, + "transactions": { + "type": "array", + "items": { + "$ref": "#/transaction" + } + }, + "ussd_codes": { + "type": "array", + "items": { + "$ref": "#/ussd-code" + } + } + } + }, + + "metadata": { + "type": "object", + "properties": { + "spec": { + "type": "string" + }, + "date_updated": { + "$ref": "#/date" + }, + "url": { + "type": "string" + } + } + }, + + "transaction": { + "type": "object", + "properties": { + "name": { + "$ref": "#/name" + }, + "classes": { + "type": "array", + "items": { + "$ref": "#/class" + } + }, + "amount_input": { + "type": "boolean" + } + } + }, + + "class": { + "type": "object", + "properties": { + "name": { + "$ref": "#/name" + }, + "ranges": { + "type": "array", + "items": { + "$ref": "#/range" + } + }, + "amount": { + "$ref": "#/cost" + }, + "message": { + "type": "string" + } + } + }, + + "range": { + "type": "object", + "properties": { + "low": { + "$ref": "#/cost" + }, + "high": { + "$ref": "#/cost" + }, + "amount": { + "$ref": "#/cost" + } + } + }, + + "cost": { + "type": ["number", "string"] + }, + + "ussd-code": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + } + } + }, + + "name": { + "type": "string" + }, + + "date": { + "type": "string" + }, + + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "statusCode": { + "type": "integer" + } + }, + "required": ["message", "name", "statusCode"] + } +} diff --git a/test/.eslintrc.yml b/test/.eslintrc.yml new file mode 100644 index 0000000..0a4d044 --- /dev/null +++ b/test/.eslintrc.yml @@ -0,0 +1,18 @@ +env: + es6: true + node: true + mocha: true +extends: 'eslint:recommended' +rules: + indent: + - error + - 2 + linebreak-style: + - error + - unix + quotes: + - error + - single + semi: + - error + - always diff --git a/test/elbow/404.json b/test/elbow/404.json new file mode 100644 index 0000000..67f3a58 --- /dev/null +++ b/test/elbow/404.json @@ -0,0 +1,17 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/404", + "description": "Fetching data file for a missing network", + "methods": ["get"], + + "status": 404, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost-missing-amount-param.json b/test/elbow/cost-missing-amount-param.json new file mode 100644 index 0000000..d37b001 --- /dev/null +++ b/test/elbow/cost-missing-amount-param.json @@ -0,0 +1,22 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "Missing 'amount' parameter", + "methods": ["post"], + "params": { + "network": "mpesa", + "transactionType": "withdrawal", + "transactor": "agent" + }, + + "statusCode": 400, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost-missing-network-param.json b/test/elbow/cost-missing-network-param.json new file mode 100644 index 0000000..7a90b48 --- /dev/null +++ b/test/elbow/cost-missing-network-param.json @@ -0,0 +1,22 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "Missing 'network' parameter", + "methods": ["post"], + "params": { + "amount": 5000, + "transactionType": "withdrawal", + "transactor": "agent" + }, + + "statusCode": 400, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost-missing-transactionType-param.json b/test/elbow/cost-missing-transactionType-param.json new file mode 100644 index 0000000..c0de12d --- /dev/null +++ b/test/elbow/cost-missing-transactionType-param.json @@ -0,0 +1,22 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "Missing 'transactionType' parameter", + "methods": ["post"], + "params": { + "network": "mpesa", + "amount": 5000, + "transactor": "agent" + }, + + "statusCode": 400, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost-missing-transactor-param.json b/test/elbow/cost-missing-transactor-param.json new file mode 100644 index 0000000..485fb93 --- /dev/null +++ b/test/elbow/cost-missing-transactor-param.json @@ -0,0 +1,22 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "Missing 'transactor' parameter", + "methods": ["post"], + "params": { + "network": "mpesa", + "amount": 5000, + "transactionType": "withdrawal" + }, + + "statusCode": 400, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost-string-amount-param.json b/test/elbow/cost-string-amount-param.json new file mode 100644 index 0000000..e76936d --- /dev/null +++ b/test/elbow/cost-string-amount-param.json @@ -0,0 +1,23 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "String 'amount' parameter", + "methods": ["post"], + "params": { + "network": "mpesa", + "amount": "5000", + "transactionType": "withdrawal", + "transactor": "agent" + }, + + "statusCode": 400, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/cost.json b/test/elbow/cost.json new file mode 100644 index 0000000..c175da2 --- /dev/null +++ b/test/elbow/cost.json @@ -0,0 +1,22 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/cost", + "description": "Calculating cost of a transaction", + "methods": ["post"], + "params": { + "network": "mpesa", + "amount": 5000, + "transactionType": "withdrawal", + "transactor": "agent" + }, + + "type": "object", + "properties": { + "cost": { + "type": "number" + } + }, + "required": ["cost"] +} diff --git a/test/elbow/networks-one-404.json b/test/elbow/networks-one-404.json new file mode 100644 index 0000000..ce95c40 --- /dev/null +++ b/test/elbow/networks-one-404.json @@ -0,0 +1,17 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/networks/404", + "description": "Fetching data file for a missing network", + "methods": ["get"], + + "status": 404, + "type": "object", + "properties": { + "error": { + "$ref": "http://localhost:9667/definitions.json#/error" + } + }, + "required": ["error"] +} diff --git a/test/elbow/networks-one.json b/test/elbow/networks-one.json new file mode 100644 index 0000000..4e42929 --- /dev/null +++ b/test/elbow/networks-one.json @@ -0,0 +1,16 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/networks/mpesa", + "description": "Fetching data file for a single network", + "methods": ["get"], + + "type": "object", + "properties": { + "network": { + "$ref": "http://localhost:9667/definitions.json#/network" + } + }, + "required": ["network"] +} diff --git a/test/elbow/networks.json b/test/elbow/networks.json new file mode 100644 index 0000000..849d74e --- /dev/null +++ b/test/elbow/networks.json @@ -0,0 +1,20 @@ + +{ + "$schema": "http://json-schema.org/schema#", + + "endpoint": "/networks", + "description": "Fetching (and validating) data files for all networks", + "methods": ["get"], + + "type": "object", + "properties": { + "networks": { + "type": "array", + "items": { + "$ref": "http://localhost:9667/definitions.json#/network" + }, + "minItems": 1 + } + }, + "required": ["networks"] +} diff --git a/test/test.index.js b/test/test.index.js new file mode 100644 index 0000000..23077b1 --- /dev/null +++ b/test/test.index.js @@ -0,0 +1,43 @@ +/** + * The MIT License (MIT) + * Copyright (c) 2016 GochoMugo + * Copyright (c) 2016 Forfuture, LLC + * + * Our tests + */ + + +// built-in modules +const path = require('path'); + + +// npm-installed modules +const elbow = require('elbow'); +const express = require('express'); + + +// own modules +const app = require('../app'); + + +// module variables +const schemaDir = path.resolve(__dirname, '../schema'); +const staticServer = express(); +const staticServerPort = 9667; + + +before(function(done) { + staticServer.use(express.static(schemaDir)); + staticServer.listen(staticServerPort, done); +}); + + +describe('E2E tests for API', function() { + const port = 9666; + + before(function(done) { + app.run({ port }, done); + }); + + elbow.run(it, `http://localhost:${port}/api/`, path.join(__dirname, 'elbow')); +}); diff --git a/web/_includes/header.html b/web/_includes/header.html index 4bc4a22..6f33b46 100644 --- a/web/_includes/header.html +++ b/web/_includes/header.html @@ -24,7 +24,7 @@
  • - API + API beta