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