├── .nvmrc ├── .gitignore ├── test ├── mocha.opts ├── support │ ├── globals.js │ └── integration_helper.js ├── unit │ ├── errors.test.js │ ├── habitica.test.js │ └── connection.test.js └── integration │ └── index.test.js ├── .npmignore ├── .editorconfig ├── tasks ├── drop-db.js ├── deploy-docs.js └── start-server.js ├── .jsdoc.conf.json ├── .mversionrc ├── CHANGELOG.md ├── .travis.yml ├── package.json ├── lib ├── errors.js └── connection.js ├── README.md └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v6 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | docs/ 4 | .publish 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --require test/support/globals 3 | -------------------------------------------------------------------------------- /test/support/globals.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai') 2 | 3 | chai.use(require('chai-as-promised')) 4 | 5 | global.expect = chai.expect 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | src/* 3 | tasks/ 4 | test/ 5 | docs/ 6 | .publish/ 7 | .travis.yml 8 | .editorconfig 9 | .nvmrc 10 | .mversionrc 11 | .jsdoc.conf.json 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /tasks/drop-db.js: -------------------------------------------------------------------------------- 1 | var mongo = require('mongodb').MongoClient 2 | var dbUri = process.env.HABITICA_DB_URI || 'mongodb://localhost/habitica-node-test' 3 | 4 | mongo.connect(dbUri, function (err, db) { 5 | if (err) throw err 6 | 7 | db.dropDatabase(function () { 8 | db.close() 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /.jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": [ 4 | "lib", 5 | "package.json", 6 | "README.md", 7 | "index.js" 8 | ], 9 | "includePattern": ".js$" 10 | }, 11 | "opts": { 12 | "destination": "./docs/", 13 | "template": "node_modules/minami" 14 | }, 15 | "plugins": [ 16 | "plugins/markdown" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tasks/deploy-docs.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var ghpages = require('gh-pages') 4 | var path = require('path') 5 | var version = require('../package.json').version 6 | 7 | ghpages.publish(path.join(__dirname, '../docs/habitica/' + version), (err) => { 8 | if (err) { 9 | console.error(err) 10 | return 11 | } 12 | 13 | console.info('Docs published!') 14 | }) 15 | -------------------------------------------------------------------------------- /.mversionrc: -------------------------------------------------------------------------------- 1 | { 2 | "commitMessage": "%s", 3 | "tagName": "v%s", 4 | "scripts": { 5 | "preupdate": "echo 'Bumping version and running tests'", 6 | "precommit": "npm test", 7 | "postcommit": "git push && git push --tags && npm publish", 8 | "postupdate": "npm run docs:deploy && echo 'Updated to version %s in manifest files and deployed docs'" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | ## >3 5 | 6 | See [Releases](https://github.com/crookedneighbor/habitica-node/releases) for changelog entries 7 | 8 | ## 2.1.0 9 | 10 | * Add user.getBuyableGear method 11 | * Fix docs links 12 | 13 | ## 2.0.5 14 | 15 | * Upgrade superagent to v2 and remove q dependency 16 | 17 | ## 2.0.4 18 | 19 | * Upgrade various dependencies 20 | 21 | ## 2.0.0 - 2.0.3 22 | 23 | ### Breaking Changes 24 | 25 | * All api methods now resolve proper errors instead of randomly throwing strings 26 | -------------------------------------------------------------------------------- /tasks/start-server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | var exec = require('child_process').exec 3 | 4 | var PATH_TO_HABITICA = process.env.PATH_TO_HABITICA || '../../habitica' 5 | 6 | var args = process.argv 7 | var command 8 | 9 | if (args.length > 2) { 10 | command = args.splice(2).join(' ') 11 | } 12 | 13 | process.env.PORT = process.env.HABITICA_PORT || 3321 14 | process.env.NODE_DB_URI = process.env.HABITICA_DB_URI || 'mongodb://localhost/habitica-node-test' 15 | process.env.WEB_CONCURRENCY = 0 // must be set to start server in dev mode 16 | 17 | console.log('If running standalone, use the following command to set your environmental variables') 18 | console.log('export PORT="' + process.env.PORT + '"; export NODE_DB_URI="' + process.env.NODE_DB_URI + '"') 19 | var server = require(PATH_TO_HABITICA + '/website/server/') 20 | 21 | server.listen(process.env.PORT, function () { 22 | if (command) { 23 | exec(command, function (error, stdout, stderr) { 24 | if (error) { 25 | console.error(error) 26 | } 27 | if (stdout) console.log(stdout) 28 | if (stderr) { 29 | console.error(stderr) 30 | process.exit(1) 31 | } 32 | 33 | process.exit() 34 | }) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | sudo: required 5 | addons: 6 | apt: 7 | sources: 8 | - ubuntu-toolchain-r-test 9 | packages: 10 | - g++-4.8 11 | before_install: 12 | - $CXX --version 13 | - npm i -g npm@4 14 | - sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 15 | - echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | 16 | sudo tee /etc/apt/sources.list.d/mongodb.list 17 | - sudo apt-get update 18 | - sudo apt-get install mongodb-org-server 19 | - git clone https://github.com/HabitRPG/habitica.git ../habitica 20 | - until nc -z localhost 27017; do echo Waiting for MongoDB; sleep 1; done 21 | - cd ../habitica && cp config.json.example config.json && npm install && cd - 22 | env: 23 | global: 24 | - CXX=g++-4.8 25 | notifications: 26 | email: 27 | on_success: change 28 | on_failure: always 29 | slack: 30 | secure: cJf/QtAXyM14n0yESdEWh3OxU5JZfD5vLjxzbOn3uQIV2aXYtB5H66qUl1Vr3vMRwGEctP8+GU84zPLNrxXvQdSzYVU+Kco0TBzfOnYr7fngm5lFhcoK4PQF30O4P4NKSiq/6t8K9vAQA+41kzzLQnibcgbW6ttwSrY+Uss/Ys8F2OCv5eVfoz0Zr+5IWqU8XoHbhB3exKyT9r3QKl9+907WH56ququ848ZTDVJXBy2z0ML09R8rL5F+YCWuXebS4XqIiTD1Wy38zIF+efnLI81Kb8tQWLrZD/oniwLUTwZrtnK1jMvKqcB+wyek10vxww8yXamtwT9LTjHmZRIRwKaSRkQkeKUWtB5yR0cfD0mCeSEeM846sZC7E/qHOv5zKPqnm3UQNOdjgyzWvJFIDNQZVa/UllpjTrBIHG7d1vT5VSGor6VdExUijAdmU810idfMEa4Ed7NefOCgVV9L9aJ0XJFI2BZRAo9gE52WfYfC4bJdd0EP86F1N2XnCYT2ZqkkCbtHQ8Cljpe+I8zpIWyvsATWwLFMsN7AThVO7EjhZ/Uk8bW4G2qdalBwC8cTi73swzdrqoWyc+Uj9Dm4hl47cfMLbCXHpzcHbCzgLm8Bl87nNwd+sub7Hrp8oYwdtUOCMsIeDx0or6fhOOba91UGVTn91t6gDiPV6aSwEZg= 31 | -------------------------------------------------------------------------------- /test/unit/errors.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var errors = require('../../lib/errors') 4 | var HabiticaApiError = errors.HabiticaApiError 5 | var UnknownConnectionError = errors.UnknownConnectionError 6 | 7 | describe('Errors', function () { 8 | describe('UnknownConnectionError', function () { 9 | it('instantiates an error', function () { 10 | var error = new UnknownConnectionError() 11 | 12 | expect(error).to.be.an.instanceof(HabiticaApiError) 13 | expect(error.name).to.equal('UnknownConnectionError') 14 | expect(error.message).to.equal('An unknown error occurred') 15 | }) 16 | 17 | it('saves original error', function () { 18 | var originalError = new Error('foo') 19 | var error = new UnknownConnectionError(originalError) 20 | 21 | expect(error.originalError).to.equal(originalError) 22 | }) 23 | 24 | it('does not overwrite error from original error', function () { 25 | var originalError = new Error('foo') 26 | var error = new UnknownConnectionError(originalError) 27 | 28 | expect(error.message).to.equal('An unknown error occurred') 29 | }) 30 | }) 31 | 32 | describe('HabiticaApiError', function () { 33 | it('instantiates an API error', function () { 34 | var responseError = { 35 | status: 404, 36 | type: 'NotFound', 37 | message: 'User not found.' 38 | } 39 | var error = new HabiticaApiError(responseError) 40 | 41 | expect(error.name).to.equal('HabiticaApiNotFoundError') 42 | expect(error.type).to.equal('NotFound') 43 | expect(error.status).to.equal(404) 44 | expect(error.message).to.equal('User not found.') 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/unit/habitica.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Habitica = require('../../index') 4 | var errors = require('../../lib/errors') 5 | var requireAgain = require('require-again') 6 | 7 | describe('Habitica Api', function () { 8 | beforeEach(function () { 9 | this.api = new Habitica({id: 'myUuid', apiToken: 'myToken'}) 10 | }) 11 | 12 | it('requires Promises', function () { 13 | /* eslint-disable */ 14 | var PromiseObject = Promise 15 | 16 | Promise = undefined 17 | 18 | expect(function () { 19 | requireAgain('../../index') 20 | }).to.throw('Promise could not be found in this context. You must polyfill it to use this module.') 21 | 22 | Promise = PromiseObject 23 | /* eslint-enable */ 24 | }) 25 | 26 | describe('ApiError', function () { 27 | it('is the HabiticaApiError object', function () { 28 | expect(Habitica.ApiError).to.equal(errors.HabiticaApiError) 29 | }) 30 | }) 31 | 32 | describe('UnknownConnectionError', function () { 33 | it('is the HabiticaApiError object', function () { 34 | expect(Habitica.UnknownConnectionError).to.equal(errors.UnknownConnectionError) 35 | }) 36 | }) 37 | 38 | describe('#getOptions', function () { 39 | it('returns an object with the configured options', function () { 40 | var options = this.api.getOptions() 41 | 42 | expect(options).to.deep.equal({ 43 | id: 'myUuid', 44 | apiToken: 'myToken', 45 | endpoint: 'https://habitica.com', 46 | platform: 'Habitica-Node' 47 | }) 48 | }) 49 | }) 50 | 51 | describe('#setOptions', function () { 52 | it('sets new options', function () { 53 | this.api.setOptions({id: 'newUuid', apiToken: 'newToken'}) 54 | 55 | var options = this.api.getOptions() 56 | 57 | expect(options.id).to.equal('newUuid') 58 | expect(options.apiToken).to.equal('newToken') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /test/support/integration_helper.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var mongo = require('mongodb').MongoClient 4 | var generateRandomUserName = require('uuid').v4 5 | var superagent = require('superagent') 6 | 7 | function generateUser (update, connection) { 8 | var username = generateRandomUserName() 9 | var password = 'password' 10 | var email = username + '@example.com' 11 | 12 | return superagent.post(`localhost:${process.env.PORT}/api/v3/user/auth/local/register`) 13 | .accept('application/json') 14 | .send({ 15 | username, 16 | email, 17 | password, 18 | confirmPassword: password 19 | }).then((res) => { 20 | var user = res.body.data 21 | var userCreds = { 22 | id: user.id, 23 | apiToken: user.apiToken 24 | } 25 | 26 | if (connection) { 27 | connection.setOptions(userCreds) 28 | } 29 | 30 | return updateDocument('users', user.id, update) 31 | }) 32 | } 33 | 34 | function updateDocument (collectionName, uuid, update) { 35 | if (!update) { return } 36 | 37 | if (!process.env.NODE_DB_URI) { 38 | throw new Error('No process.env.NODE_DB_URI specified. Type `export NODE_DB_URI=\'mongodb://localhost/habitica-node-test\'` on the command line') 39 | } 40 | 41 | return new Promise((resolve, reject) => { 42 | mongo.connect(process.env.NODE_DB_URI, (connectionError, db) => { 43 | if (connectionError) { 44 | reject(new Error(`Error connecting to database when updating ${collectionName} collection: ${connectionError}`)) 45 | } 46 | 47 | var collection = db.collection(collectionName) 48 | 49 | collection.update({ _id: uuid }, { $set: update }, (updateError, result) => { 50 | if (updateError) { 51 | reject(new Error(`Error updating ${collectionName}: ${updateError}`)) 52 | } 53 | resolve() 54 | }) 55 | }) 56 | }) 57 | } 58 | 59 | module.exports = { 60 | generateUser: generateUser, 61 | updateDocument: updateDocument 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "habitica", 3 | "version": "4.0.0", 4 | "description": "A node wrapper for the habitica api", 5 | "main": "./index.js", 6 | "scripts": { 7 | "docs": "npm run docs:clean && jsdoc --configure .jsdoc.conf.json", 8 | "watch": "npm-watch", 9 | "docs:deploy": "npm run docs && node tasks/deploy-docs.js", 10 | "docs:clean": "rm -rf docs/*.html", 11 | "lint": "standard --verbose | snazzy", 12 | "test:support:drop-db": "node tasks/drop-db.js", 13 | "test:support:server": "node tasks/start-server", 14 | "test": "npm run lint && npm run test:unit && npm run test:integration-with-server", 15 | "test:integration": "mocha test/integration", 16 | "test:integration-with-server": "npm run test:support:drop-db && node tasks/start-server npm run test:integration", 17 | "test:unit": "mocha test/unit" 18 | }, 19 | "watch": { 20 | "docs": [ 21 | "index.js", 22 | "lib/*.js" 23 | ] 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+ssh://git@github.com/crookedneighbor/habitica-node.git" 28 | }, 29 | "keywords": [ 30 | "habitica", 31 | "habitrpg" 32 | ], 33 | "author": "Blade Barringer ", 34 | "contributors": [ 35 | { 36 | "name": "Max Buras", 37 | "email": "max.thomae@googlemail.com" 38 | }, 39 | { 40 | "name": "Nick Tomlin", 41 | "email": "nick.tomlin@gmail.com" 42 | } 43 | ], 44 | "license": "ISC", 45 | "bugs": { 46 | "url": "https://github.com/crookedneighbor/habitica-node/issues" 47 | }, 48 | "homepage": "https://github.com/crookedneighbor/habitica-node#readme", 49 | "devDependencies": { 50 | "chai": "^3.2.0", 51 | "chai-as-promised": "^5.1.0", 52 | "gh-pages": "^0.11.0", 53 | "jsdoc": "^3.4.0", 54 | "kerberos": "0.0.22", 55 | "minami": "^1.1.1", 56 | "mocha": "^3.0.2", 57 | "mongodb": "^2.1.3", 58 | "nock": "^9.0.0", 59 | "npm-watch": "^0.1.6", 60 | "require-again": "^2.0.0", 61 | "snazzy": "^4.0.0", 62 | "standard": "^10.0.0", 63 | "uuid": "^3.0.0" 64 | }, 65 | "dependencies": { 66 | "superagent": "^3.5.2" 67 | }, 68 | "standard": { 69 | "globals": [ 70 | "$", 71 | "after", 72 | "afterEach", 73 | "before", 74 | "beforeEach", 75 | "context", 76 | "describe", 77 | "expect", 78 | "it", 79 | "xcontext", 80 | "xdescribe", 81 | "xit" 82 | ] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function CustomError (message) { 4 | /** A translated error message you can provide for your user 5 | * @memberof HabiticaApiError 6 | */ 7 | this.message = message 8 | 9 | Error.captureStackTrace && Error.captureStackTrace(this, this.constructor) 10 | } 11 | 12 | CustomError.prototype = Object.create(Error.prototype) 13 | 14 | /** 15 | * @constructor HabiticaApiError 16 | * @description Returned when the API request returns a HTTP status between 400 and 500 17 | * 18 | * @example Use the error objects on the Habitica package to write an error handler 19 | * var Habitica = require('habitica') 20 | * var api = new Habitica({ 21 | * // setup client 22 | * }) 23 | * 24 | * api.get('/user').then(() => { 25 | * // will never get here 26 | * }).catch((err) => { 27 | * if (err instanceof Habitica.ApiError) { 28 | * // likely a validation error from 29 | * // the API request 30 | * console.log(err.message) 31 | * } else if (err instanceof Habitica.UnknownConnectionError) { 32 | * // either the Habitica API is down 33 | * // or there is no internet connection 34 | * console.log(err.originalError) 35 | * } else { 36 | * // there is something wrong with your integration 37 | * // such as a syntax error or other problem 38 | * console.log(err) 39 | * } 40 | * }) 41 | */ 42 | function HabiticaApiError (options) { 43 | options = options || {} 44 | 45 | CustomError.call(this, options.message) 46 | 47 | /** The status code of the HTTP request. Will be a number >= `400` and <= `500` 48 | * @memberof HabiticaApiError 49 | */ 50 | this.status = options.status 51 | 52 | /** A type coresponding to the status code. For instance, an error with a status of `404` will be type `NotFound` 53 | * @memberof HabiticaApiError 54 | */ 55 | this.type = options.type 56 | this.name = 'HabiticaApi' + options.type + 'Error' 57 | } 58 | 59 | HabiticaApiError.prototype = Object.create(CustomError.prototype) 60 | 61 | /** 62 | * @constructor UnknownConnectionError 63 | * 64 | * @description Returned when an error could not be parsed from a failed request. Most likely when there is not an internet connection. 65 | * @example See {@link HabiticaApiError} for a full example of how to use it when handling request errors. 66 | * api.get('/user').then(() => { 67 | * // assuming there is no internet 68 | * // connection, so user never 69 | * // gets here 70 | * }).catch((err) => { 71 | * err.message // 'An unknown error occurred' 72 | * console.error(err.originalError) 73 | * }) 74 | */ 75 | function UnknownConnectionError (err) { 76 | CustomError.call(this, 'An unknown error occurred') 77 | 78 | /** The original error from the failed request 79 | * @memberof UnknownConnectionError 80 | */ 81 | this.originalError = err 82 | this.name = 'UnknownConnectionError' 83 | } 84 | 85 | UnknownConnectionError.prototype = Object.create(HabiticaApiError.prototype) 86 | 87 | module.exports = { 88 | HabiticaApiError: HabiticaApiError, 89 | UnknownConnectionError: UnknownConnectionError 90 | } 91 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var superagent = require('superagent') 4 | var errors = require('./errors') 5 | var HabiticaApiError = errors.HabiticaApiError 6 | var UnknownConnectionError = errors.UnknownConnectionError 7 | 8 | var DEFAULT_ENDPOINT = 'https://habitica.com/' 9 | var DEFAULT_PLATFORM = 'Habitica-Node' 10 | var TOP_LEVEL_ROUTES = [ 11 | 'logout', 12 | 'export', 13 | 'email', 14 | 'qr-code', 15 | 'amazon', 16 | 'iap', 17 | 'paypal', 18 | 'stripe' 19 | ] 20 | var TOP_LEVEL_ROUTES_REGEX = new RegExp('^/(' + TOP_LEVEL_ROUTES.join('|') + ').*') 21 | 22 | function formatError (err) { 23 | var connectionError, status, data 24 | 25 | if (err.response && err.response.error) { 26 | status = err.response.error.status 27 | data = JSON.parse(err.response.text) 28 | 29 | connectionError = new HabiticaApiError({ 30 | type: data.error, 31 | status: status, 32 | message: data.message 33 | }) 34 | } 35 | 36 | if (!connectionError) { 37 | connectionError = new UnknownConnectionError(err) 38 | } 39 | 40 | return connectionError 41 | } 42 | 43 | function normalizeEndpoint (url) { 44 | var lastCharIndex = url.length - 1 45 | var lastChar = url[lastCharIndex] 46 | 47 | if (lastChar === '/') { 48 | url = url.substring(0, lastCharIndex) 49 | } 50 | 51 | return url 52 | } 53 | 54 | function Connection (options) { 55 | options.endpoint = options.endpoint || DEFAULT_ENDPOINT 56 | options.platform = options.platform || DEFAULT_PLATFORM 57 | 58 | this.setOptions(options) 59 | } 60 | 61 | Connection.prototype.getOptions = function () { 62 | return { 63 | id: this._id, 64 | apiToken: this._apiToken, 65 | endpoint: this._endpoint, 66 | platform: this._platform 67 | } 68 | } 69 | 70 | Connection.prototype.setOptions = function (creds) { 71 | creds = creds || {} 72 | 73 | if (creds.hasOwnProperty('id')) { 74 | this._id = creds.id 75 | } 76 | if (creds.hasOwnProperty('apiToken')) { 77 | this._apiToken = creds.apiToken 78 | } 79 | if (creds.hasOwnProperty('endpoint')) { 80 | this._endpoint = normalizeEndpoint(creds.endpoint) 81 | } 82 | if (creds.hasOwnProperty('platform')) { 83 | this._platform = creds.platform 84 | } 85 | if (creds.hasOwnProperty('errorHandler')) { 86 | this._errorHandler = creds.errorHandler 87 | } 88 | } 89 | 90 | Connection.prototype.get = function (route, options) { 91 | return this._router('get', route, options) 92 | } 93 | 94 | Connection.prototype.post = function (route, options) { 95 | return this._router('post', route, options) 96 | } 97 | 98 | Connection.prototype.put = function (route, options) { 99 | return this._router('put', route, options) 100 | } 101 | 102 | Connection.prototype.del = function (route, options) { 103 | return this._router('del', route, options) 104 | } 105 | 106 | Connection.prototype.delete = Connection.prototype.del 107 | 108 | Connection.prototype._router = function (method, route, options) { 109 | var request, prefix 110 | 111 | options = options || {} 112 | 113 | if (TOP_LEVEL_ROUTES_REGEX.test(route)) { 114 | prefix = '' 115 | } else { 116 | prefix = '/api/v3' 117 | } 118 | 119 | request = superagent[method](this._endpoint + prefix + route) 120 | .accept('application/json') 121 | .set('x-client', this._platform) 122 | 123 | if (this._id && this._apiToken) { 124 | request 125 | .set('x-api-user', this._id) 126 | .set('x-api-key', this._apiToken) 127 | } 128 | 129 | request 130 | .query(options.query) 131 | .send(options.send) 132 | 133 | return request.then(function (response) { 134 | return response.body 135 | }).catch(function (err) { 136 | var formattedError = formatError(err) 137 | 138 | if (typeof this._errorHandler === 'function') { 139 | return Promise.reject(this._errorHandler(formattedError)) 140 | } else { 141 | throw formattedError 142 | } 143 | }.bind(this)) 144 | } 145 | 146 | module.exports = Connection 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Habitica 2 | [![Build Status](https://travis-ci.org/crookedneighbor/habitica-node.svg?branch=master)](https://travis-ci.org/crookedneighbor/habitica-node) 3 | --- 4 | 5 | A very thin wrapper for the Habitica API 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install habitica --save 11 | ``` 12 | 13 | ## Usage 14 | 15 | This package is intentionally light and unopinionated. It should be used in conjunction with [Habitica's API documentation](https://habitica.com/apidoc/). 16 | 17 | You can find your user id and api token [here](https://habitica.com/#/options/settings/api). 18 | 19 | ## Setup 20 | 21 | The first thing you need to do is instantiate your client. All the configuration properties are optional and can be set later. 22 | 23 | ```js 24 | var Habitica = require('habitica'); 25 | var api = new Habitica({ 26 | id: 'your-habitica.com-user-id', 27 | apiToken: 'your-habitica.com-api-token', 28 | endpoint: 'http://custom-url.com/', // defaults to https://habitica.com/ 29 | platform: 'Your-Integration-Name' // defaults to Habitica-Node 30 | }); 31 | ``` 32 | 33 | Using the `register` or `localLogin` methods will set the User Id and API token automatically. 34 | 35 | ```js 36 | api.register( 37 | 'username', 38 | 'email', 39 | 'password' 40 | ).then((res) => { 41 | var user = res.data 42 | 43 | // do something with user 44 | // hit a route with the authenticated client 45 | return api.get('/groups') 46 | }).then((res) => { 47 | var groups = res.data 48 | 49 | // do something 50 | }); 51 | 52 | api.localLogin( 53 | 'username or email', 54 | 'password' 55 | ).then((res) => { 56 | var creds = res.data 57 | 58 | // do something with the credentials 59 | // hit a route with the authenticated client 60 | return api.get('/groups') 61 | }).then((res) => { 62 | var groups = res.data 63 | 64 | // do something 65 | }); 66 | ``` 67 | 68 | If your integration prompts the user to enter their credentials, you can use the `setOptions` method. 69 | 70 | ```js 71 | api.setOptions({ 72 | id: 'the-uuid', 73 | apiToken: 'the-api-token' 74 | }) 75 | ``` 76 | 77 | ## Request Methods 78 | 79 | There are four main methods to make requests to the API. `get`, `post`, `put` and `del`. Each corresponds to one of the main HTTP verbs. 80 | 81 | `get` takes an optional second argument that is an object that gets converted to a query string. The rest have an optional second argument that is the post body and a third optional argument that will be converted to a query string. 82 | 83 | Each method returns a promise which resolves the raw data back from the API. The data will reside on the data property. 84 | 85 | ```js 86 | api.get('/user').then((res) => { 87 | var user = res.data 88 | 89 | return api.put('/user', { 90 | 'profile.name': 'New Name' 91 | }) 92 | }).then((res) => { 93 | var user = res.data 94 | user.profile.name // 'New Name' 95 | 96 | return api.post('/tasks/user', { 97 | type: 'todo', 98 | text: 'A new todo' 99 | }) 100 | }).then((res) => { 101 | var task = res.data 102 | 103 | return api.post('/tasks/' + task.id + '/score/up') 104 | }).then((res) => { 105 | // Your task was scored! 106 | }).catch((err) => { 107 | if (err.message) { 108 | // API Error, display the message 109 | } else { 110 | // something else in your integration went wrong 111 | } 112 | }) 113 | ``` 114 | 115 | For full documentation with examples visit [the docs site](http://crookedneighbor.github.io/habitica-node/). 116 | 117 | ## Documentation 118 | 119 | The documentation is generated automatically using [JSDoc](http://usejsdoc.org/). 120 | 121 | ## Testing 122 | 123 | To run all the tests: 124 | 125 | ``` 126 | $ npm t 127 | ``` 128 | 129 | * The bulk of the tests are integration tets that expect a Habitica dev instance to be running. 130 | 131 | * A mongodb instance must be running already in order to run the tests locally. 132 | 133 | * By default, the test infrastructure assumes that the repo for Habitica is '../../habitica', relative to the test directory. You may pass in your own path by exporting the environmental variable `PATH_TO_HABITICA`. 134 | 135 | ``` 136 | $ export PATH_TO_HABITICA='../../some/other/path'; 137 | ``` 138 | 139 | * By default, the app will be served on port 3321. This can be configured with the environmental variable `HABITICA_PORT`: 140 | 141 | ``` 142 | $ export HABITICA_PORT=3001; 143 | ``` 144 | 145 | * By default, the mongodb uri is 'mongodb://localhost/habitica-node-test'. You can configure this variable with the environmental variable `HABITICA_DB_URI`: 146 | 147 | ``` 148 | $ export HABITICA_DB_URI='mongodb://localhost/some-other-db'; 149 | ``` 150 | 151 | ## Support 152 | 153 | This module requires the `Promise` object to function. If you are using this module in a context without `Promise`s (such as Browserifying for IE9), you will need to polyfill them. 154 | 155 | Supports Node >= 4 156 | -------------------------------------------------------------------------------- /test/integration/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Habitica = require('../../') 4 | var helper = require('../support/integration_helper') 5 | var updateDocument = helper.updateDocument 6 | var generateUser = helper.generateUser 7 | var generateRandomUserName = require('uuid').v4 8 | 9 | describe('Habitica', function () { 10 | beforeEach(function () { 11 | this.api = new Habitica({ 12 | endpoint: `localhost:${process.env.PORT}/` 13 | }) 14 | return generateUser(null, this.api) 15 | }) 16 | 17 | describe('#register', function () { 18 | beforeEach(function () { 19 | this.api.setOptions({ 20 | id: null, 21 | apiToken: null 22 | }) 23 | this.username = generateRandomUserName() 24 | this.password = 'password' 25 | this.email = this.username + '@example.com' 26 | }) 27 | 28 | context('Successful', function () { 29 | it('registers for a new account', function () { 30 | return this.api.register(this.username, this.email, this.password).then(() => { 31 | return this.api.get('/user') 32 | }).then((body) => { 33 | var user = body.data 34 | 35 | expect(user.auth.local.username).to.equal(this.username) 36 | expect(user.auth.local.email).to.equal(this.email) 37 | }) 38 | }) 39 | 40 | it('returns a response with the new user', function () { 41 | return this.api.register(this.username, this.email, this.password).then((body) => { 42 | var user = body.data 43 | var options = this.api.getOptions() 44 | 45 | expect(user._id).to.equal(options.id) 46 | expect(user.apiToken).to.equal(options.apiToken) 47 | expect(user.auth.local.username).to.equal(this.username) 48 | expect(user.auth.local.email).to.equal(this.email) 49 | }) 50 | }) 51 | 52 | it('sets id and api token to new user', function () { 53 | expect(this.api._id).to.equal(undefined) 54 | expect(this.api._apiToken).to.equal(undefined) 55 | 56 | return this.api.register(this.username, this.email, this.password).then((body) => { 57 | var user = body.data 58 | var options = this.api.getOptions() 59 | 60 | expect(options.id).to.be.equal(user._id) 61 | expect(options.apiToken).to.be.equal(user.apiToken) 62 | }) 63 | }) 64 | }) 65 | 66 | context('Invalid Input', function () { 67 | it('resolves with error when username is not provided', function () { 68 | return expect(this.api.register('', this.email, this.password)).to.eventually.be.rejected.and.have.property('status', 400) 69 | }) 70 | 71 | it('resolves with error when email is not provided', function () { 72 | return expect(this.api.register(this.username, '', this.password)).to.eventually.be.rejected 73 | }) 74 | 75 | it('resolves with error when password is not provided', function () { 76 | return expect(this.api.register(this.username, this.email, '')).to.eventually.be.rejected 77 | }) 78 | 79 | it('resolves with error when email is not valid', function () { 80 | return expect(this.api.register(this.username, 'not.a.valid.email@example', this.password)).to.eventually.be.rejected 81 | }) 82 | }) 83 | }) 84 | 85 | describe('#localLogin', function () { 86 | beforeEach(function () { 87 | var registerApi = new Habitica({ 88 | endpoint: `localhost:${process.env.PORT}` 89 | }) 90 | this.api.setOptions({ 91 | id: null, 92 | apiToken: null 93 | }) 94 | 95 | this.username = generateRandomUserName() 96 | this.password = 'password' 97 | this.email = this.username + '@example.com' 98 | 99 | return registerApi.register(this.username, this.email, this.password) 100 | }) 101 | 102 | context('Success', function () { 103 | it('logs in with username and password', function () { 104 | return this.api.localLogin(this.username, this.password).then((body) => { 105 | var creds = body.data 106 | 107 | expect(creds.id).to.not.equal(undefined) 108 | expect(creds.apiToken).to.not.equal(undefined) 109 | }) 110 | }) 111 | 112 | it('sets id and apiToken after logging in with username', function () { 113 | return this.api.localLogin(this.username, this.password).then((body) => { 114 | var creds = body.data 115 | var options = this.api.getOptions() 116 | 117 | expect(options.id).to.be.equal(creds.id) 118 | expect(options.apiToken).to.be.equal(creds.apiToken) 119 | }) 120 | }) 121 | 122 | it('logs in with email and password', function () { 123 | return this.api.localLogin(this.email, this.password).then((body) => { 124 | var creds = body.data 125 | 126 | expect(creds.id).to.not.equal(undefined) 127 | expect(creds.apiToken).to.not.equal(undefined) 128 | }) 129 | }) 130 | 131 | it('sets id and apiToken after logging in with email', function () { 132 | return this.api.localLogin(this.email, this.password).then((body) => { 133 | var creds = body.data 134 | var options = this.api.getOptions() 135 | 136 | expect(options.id).to.be.equal(creds.id) 137 | expect(options.apiToken).to.be.equal(creds.apiToken) 138 | }) 139 | }) 140 | }) 141 | 142 | context('Failures', function () { 143 | it('resolves with error when account is not provided', function () { 144 | return expect(this.api.localLogin(null, this.password)).to.eventually.be.rejected 145 | }) 146 | 147 | it('resolves with error when account is not provided', function () { 148 | return expect(this.api.localLogin(this.username, null)).to.eventually.be.rejected 149 | }) 150 | 151 | it('resolves with error when account does not exist', function () { 152 | return expect(this.api.localLogin('not-existant', this.password)).to.eventually.be.rejected 153 | }) 154 | 155 | it('resolves with error when password does not match', function () { 156 | return expect(this.api.localLogin(this.username, 'password-not-correct')).to.eventually.be.rejected 157 | }) 158 | }) 159 | }) 160 | 161 | describe('#get', function () { 162 | it('sends a GET request to Habitica', function () { 163 | return this.api.get('/user').then((res) => { 164 | var user = res.data 165 | 166 | expect(user._id).to.equal(this.api.getOptions().id) 167 | }) 168 | }) 169 | 170 | it('can send query parameters', function () { 171 | return this.api.post('/groups', { 172 | type: 'party', 173 | name: 'Foo' 174 | }).then((res) => { 175 | return this.api.get('/groups', { 176 | type: 'party' 177 | }) 178 | }).then((res) => { 179 | var groups = res.data 180 | var party = groups.find(group => group.type === 'party') 181 | 182 | expect(party.name).to.equal('Foo') 183 | }) 184 | }) 185 | }) 186 | 187 | describe('#post', function () { 188 | it('sends a POST request to Habitica', function () { 189 | return updateDocument('users', this.api.getOptions().id, { 190 | 'stats.hp': 20, 191 | 'stats.gp': 100 192 | }).then(() => { 193 | return this.api.post('/user/buy-health-potion') 194 | }).then((res) => { 195 | var stats = res.data 196 | 197 | expect(stats.hp).to.be.greaterThan(20) 198 | }) 199 | }) 200 | 201 | it('can send a body object', function () { 202 | return this.api.post('/tasks/user', { 203 | text: 'Task Name', 204 | notes: 'Task Notes', 205 | type: 'todo' 206 | }).then((res) => { 207 | var task = res.data 208 | 209 | expect(task.text).to.equal('Task Name') 210 | expect(task.notes).to.equal('Task Notes') 211 | }) 212 | }) 213 | 214 | it('can send query parameters', function () { 215 | return updateDocument('users', this.api.getOptions().id, { 216 | 'stats.lvl': 20, 217 | 'stats.points': 20 218 | }).then(() => { 219 | return this.api.post('/user/allocate', null, { 220 | stat: 'int' 221 | }) 222 | }).then((res) => { 223 | var stats = res.data 224 | 225 | expect(stats.int).to.equal(1) 226 | }) 227 | }) 228 | }) 229 | 230 | describe('#put', function () { 231 | it('sends a PUT request to Habitica with a body object', function () { 232 | return this.api.put('/user', { 233 | 'profile.name': 'New Name' 234 | }).then((res) => { 235 | var user = res.data 236 | 237 | expect(user.profile.name).to.equal('New Name') 238 | }) 239 | }) 240 | 241 | it('can send query parameters', function () { 242 | return this.api.put('/user', { 243 | 'profile.name': 'foo' 244 | }, { 245 | userV: 1 246 | }).then((res) => { 247 | var userV = res.userV 248 | 249 | expect(userV).to.be.greaterThan(1) 250 | }) 251 | }) 252 | }) 253 | 254 | describe('#del', function () { 255 | it('sends a DEL request to Habitica', function () { 256 | return this.api.post('/tasks/user', { type: 'habit', text: 'text' }).then((res) => { 257 | return this.api.del(`/tasks/${res.data.id}`) 258 | }).then((res) => { 259 | expect(res.success).to.equal(true) 260 | }) 261 | }) 262 | 263 | it('can send a body object', function () { 264 | return this.api.del('/user', { 265 | password: 'password' 266 | }).then((res) => { 267 | expect(res.success).to.equal(true) 268 | }) 269 | }) 270 | 271 | it('can send query parameters', function () { 272 | return this.api.post('/tasks/user', { type: 'habit', text: 'text' }).then((res) => { 273 | return this.api.del(`/tasks/${res.data.id}`, null, { 274 | userV: 1 275 | }) 276 | }).then((res) => { 277 | expect(res.userV).be.greaterThan(2) 278 | }) 279 | }) 280 | }) 281 | }) 282 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Connection = require('./lib/connection') 4 | var errors = require('./lib/errors') 5 | 6 | if (!global.Promise) { 7 | throw new Error('Promise could not be found in this context. You must polyfill it to use this module.') 8 | } 9 | 10 | /** 11 | * @constructor 12 | * @description Your client to interact with the [Habitica API](https://habitica.com/apidoc/). 13 | * 14 | * @param {Object} options - The properties to configure the Habitica client 15 | * @param {String} [options.id] - The id of the user 16 | * @param {String} [options.apiToken] - The API token of the user 17 | * @param {String} [options.endpoint=https://habitica.com] - The endpoint to use 18 | * @param {String} [options.platform=Habitica-Node] - The name of your integration 19 | * @param {Function} [options.errorHandler] - A function to run when a request errors. The result of this function will be the argument passed to the `catch` in the request `Promise` 20 | * 21 | * @example 22 | * var Habitica = require('habitica') 23 | * var api = new Habitica({ 24 | * id: 'your-habitica.com-user-id', 25 | * apiToken: 'your-habitica.com-api-token', 26 | * endpoint: 'http://custom-url.com', 27 | * platform: 'The Name of Your Integration' 28 | * errorHandler: function (err) { 29 | * // handle all errors from failed requests 30 | * } 31 | * }) 32 | * @example The id and apiToken parameters are not required and can be set later. The credentials will be automatically set when using the register and localLogin methods. 33 | * var Habitica = require('habitica') 34 | * var api = new Habitica() 35 | * @example A sample error handler 36 | * var Habitica = require('habitica') 37 | * 38 | * function sendNotification (style, message) { 39 | * // logic for sending a notification to user 40 | * } 41 | * 42 | * var api = new Habitica({ 43 | * id: 'your-habitica.com-user-id', 44 | * apiToken: 'your-habitica.com-api-token', 45 | * errorHandler: function (err) { 46 | * if (err instanceof Habitica.ApiError) { 47 | * // likely a validation error from 48 | * // the API request 49 | * sendNotification('warning', err.messsage) 50 | * } else if (err instanceof Habitica.UnknownConnectionError) { 51 | * // either the Habitica API is down 52 | * // or there is no internet connection 53 | * sendNotification('danger', err.originalError.message) 54 | * } else { 55 | * // there is something wrong with your integration 56 | * // such as a syntax error or other problem 57 | * console.error(err) 58 | * } 59 | * } 60 | * }) 61 | * 62 | * api.get('/tasks/id-that-does-not-exist').then(() => { 63 | * // will never get here 64 | * return api.get('/something-else') 65 | * }).then(() => { 66 | * // will never get here 67 | * }).catch((err) => { 68 | * // before this happens, the errorHandler gets run 69 | * err // undefined because the errorHandler did not return anything 70 | * // you could leave the catch off entirely since the 71 | * // configured errorHandler does all the necessary work 72 | * // to message back to the user 73 | * }) 74 | */ 75 | function Habitica (options) { 76 | options = options || {} 77 | 78 | this._connection = new Connection(options) 79 | } 80 | 81 | /** @public 82 | * 83 | * @returns {Object} The options used to make the requests. The same as the values used {@link Habitica#setOptions|to set the options} 84 | */ 85 | Habitica.prototype.getOptions = function () { 86 | return this._connection.getOptions() 87 | } 88 | 89 | /** @public 90 | * 91 | * @param {Object} options - The properties to configure the Habitica client. If a property is not passed in, it will default to the value passed in on instantiation 92 | * @param {String} [options.id] - The id of the user 93 | * @param {String} [options.apiToken] - The API apiToken of the user 94 | * @param {String} [options.endpoint] - The endpoint to use 95 | * @param {String} [options.platform] - The name of your integration 96 | * @param {Function} [options.errorHandler] - A function to run when a request errors 97 | * 98 | * @example 99 | * api.setOptions({ 100 | * id: 'new-user-id', 101 | * apiToken: 'new-api-token', 102 | * endpoint: 'http://localhost:3000/', 103 | * platform: 'Great-Habitica-Integration', 104 | * errorHandler: yourErrorHandlerFunction 105 | * }) 106 | */ 107 | Habitica.prototype.setOptions = function (creds) { 108 | this._connection.setOptions(creds) 109 | } 110 | 111 | /** @public 112 | * 113 | * @param {String} username - The username to register with 114 | * @param {String} email - The email to register with 115 | * @param {String} password - The password to register with 116 | * 117 | * @returns {Promise} A Promise that resolves the response from the register request 118 | * 119 | * @example The id and api token will be set automatically after a sucessful registration request 120 | * api.register('username', 'email', 'password').then((res) => { 121 | * var user = res.data 122 | * }).catch((err) => { 123 | * // handle registration errors 124 | * }) 125 | */ 126 | Habitica.prototype.register = function (username, email, password) { 127 | return this.post('/user/auth/local/register', { 128 | username: username, 129 | email: email, 130 | password: password, 131 | confirmPassword: password 132 | }).then(function (res) { 133 | this.setOptions({ 134 | id: res.data._id, 135 | apiToken: res.data.apiToken 136 | }) 137 | 138 | return res 139 | }.bind(this)) 140 | } 141 | 142 | /** @public 143 | * 144 | * @param {String} usernameOrEmail - The username or email to login with 145 | * @param {String} password - The password to login with 146 | * 147 | * @returns {Promise} A Promise that resolves the response from the login request 148 | * @example The id and api token will be set automatically after a sucessful login request 149 | * api.login('username or email','password').then((res) => { 150 | * var creds = res.data 151 | * 152 | * creds.id // the user's id 153 | * creds.apiToken // the user's api token 154 | * }).catch((err) => { 155 | * // handle login errors 156 | * }) 157 | */ 158 | Habitica.prototype.localLogin = function (usernameEmail, password) { 159 | return this.post('/user/auth/local/login', { 160 | username: usernameEmail, 161 | password: password 162 | }).then(function (res) { 163 | this._connection.setOptions({ 164 | id: res.data.id, 165 | apiToken: res.data.apiToken 166 | }) 167 | 168 | return res 169 | }.bind(this)) 170 | } 171 | 172 | /** @public 173 | * 174 | * @param {String} route - The Habitica API route to use 175 | * @param {Object} [query] - Query params to send along with the request 176 | * 177 | * @returns {Promise} A Promise that resolves the response from the GET request 178 | * @example Making a basic request 179 | * api.get('/user').then((res) => { 180 | * var user = res.data 181 | * 182 | * user.profile.name // the user's display name 183 | * }) 184 | * @example A request with a query Object 185 | * api.get('/groups', { 186 | * type: 'publicGuilds,privateGuilds' 187 | * }).then((res) => { 188 | * var guilds = res.data 189 | * var guild = guilds[0] 190 | * 191 | * guild.name // the name of the group 192 | * }) 193 | * 194 | * @example Handling errors 195 | * api.get('/tasks/non-existant-id').then((res) => { 196 | * // will never get here 197 | * }).catch((err) => { 198 | * err.message // 'Task not found' 199 | * }) 200 | */ 201 | Habitica.prototype.get = function (path, query) { 202 | return this._connection.get(path, { 203 | query: query 204 | }) 205 | } 206 | 207 | /** @public 208 | * 209 | * @param {String} route - The Habitica API route to use 210 | * @param {Object} [body] - The body to send along with the request 211 | * @param {Object} [query] - Query params to send along with the request 212 | * 213 | * @returns {Promise} A Promise that resolves the response from the POST request 214 | * 215 | * @example A request with a body 216 | * api.post('/tasks/user', { 217 | * text: 'Task Name', 218 | * notes: 'Task Notes', 219 | * type: 'todo' 220 | * }).then((res) => { 221 | * var task = res.data 222 | * 223 | * task.text // 'Task Name' 224 | * }) 225 | * 226 | * @example Handling errors 227 | * api.post('/groups', { 228 | * type: 'party', 229 | * name: 'My Party' 230 | * }).then((res) => { 231 | * var party = res.data 232 | * 233 | * party.name // 'My Party' 234 | * }).catch((err) => { 235 | * // handle errors 236 | * }) 237 | */ 238 | Habitica.prototype.post = function (path, body, query) { 239 | return this._connection.post(path, { 240 | send: body, 241 | query: query 242 | }) 243 | } 244 | 245 | /** @public 246 | * 247 | * @param {String} route - The Habitica API route to use 248 | * @param {Object} [body] - The body to send along with the request 249 | * @param {Object} [query] - Query params to send along with the request 250 | * 251 | * @returns {Promise} A Promise that resolves the response from the PUT request 252 | * 253 | * @example A request with a body 254 | * api.put('/tasks/the-task-id', { 255 | * text: 'New Task Name', 256 | * notes: 'New Text Notes' 257 | * }).then((res) => { 258 | * var task = res.data 259 | * 260 | * task.text // 'New Task Name' 261 | * }) 262 | * 263 | * @example Handling errors 264 | * api.put('/groups/the-group-id', { 265 | * name: 'New Group Name' 266 | * }).then((res) => { 267 | * var group = res.data 268 | * 269 | * group.name // 'New Group Name' 270 | * }).catch((err) => { 271 | * // handle errors 272 | * }) 273 | */ 274 | Habitica.prototype.put = function (path, body, query) { 275 | return this._connection.put(path, { 276 | send: body, 277 | query: query 278 | }) 279 | } 280 | 281 | /** @public 282 | * 283 | * @param {String} route - The Habitica API route to use 284 | * @param {Object} [body] - The body to send along with the request 285 | * @param {Object} [query] - Query params to send along with the request 286 | * 287 | * @returns {Promise} A Promise that resolves the response from the DELETE request 288 | * 289 | * @example A basic request 290 | * api.del('/tasks/the-task-id').then(() => { 291 | * // The task has been deleted 292 | * }) 293 | * 294 | * @example Handling errors 295 | * api.del('/groups/the-group-id').then(() => { 296 | * // The group has been deleted 297 | * }).catch((err) => { 298 | * // handle errors 299 | * }) 300 | */ 301 | Habitica.prototype.del = function (path, body, query) { 302 | return this._connection.del(path, { 303 | send: body, 304 | query: query 305 | }) 306 | } 307 | 308 | Habitica.ApiError = errors.HabiticaApiError 309 | Habitica.UnknownConnectionError = errors.UnknownConnectionError 310 | 311 | module.exports = Habitica 312 | -------------------------------------------------------------------------------- /test/unit/connection.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var nock = require('nock') 4 | var Connection = require('../../lib/connection') 5 | var errors = require('../../lib/errors') 6 | var HabiticaApiError = errors.HabiticaApiError 7 | var UnknownConnectionError = errors.UnknownConnectionError 8 | 9 | describe('Connection', function () { 10 | beforeEach(function () { 11 | this.defaultOptions = { 12 | id: 'myUuid', 13 | apiToken: 'myToken' 14 | } 15 | }) 16 | 17 | describe('initialization', function () { 18 | it('defaults to habitica endpoint', function () { 19 | var connection = new Connection(this.defaultOptions) 20 | expect(connection._endpoint).to.equal('https://habitica.com') 21 | }) 22 | 23 | it('accepts an override for endpoint', function () { 24 | var connection = new Connection({ 25 | id: 'myUuid', 26 | apiToken: 'myToken', 27 | endpoint: 'https://someotherendpoint' 28 | }) 29 | 30 | expect(connection._endpoint).to.equal('https://someotherendpoint') 31 | }) 32 | 33 | it('removes the trailing slash to the endpoint if it is present', function () { 34 | var connection = new Connection({ 35 | id: 'myUuid', 36 | apiToken: 'myToken', 37 | endpoint: 'https://someotherendpoint/' 38 | }) 39 | 40 | expect(connection._endpoint).to.equal('https://someotherendpoint') 41 | }) 42 | 43 | it('defaults platform to Habitica-Node', function () { 44 | var connection = new Connection({ 45 | id: 'myUuid', 46 | apiToken: 'myToken' 47 | }) 48 | 49 | expect(connection._platform).to.equal('Habitica-Node') 50 | }) 51 | 52 | it('can set platform', function () { 53 | var connection = new Connection({ 54 | id: 'myUuid', 55 | apiToken: 'myToken', 56 | platform: 'my custom habitica app' 57 | }) 58 | 59 | expect(connection._platform).to.equal('my custom habitica app') 60 | }) 61 | 62 | it('can set an errorHandler', function () { 63 | var connection = new Connection({ 64 | id: 'myUuid', 65 | apiToken: 'myToken', 66 | errorHandler: () => {} 67 | }) 68 | 69 | expect(connection._errorHandler).to.be.a('function') 70 | }) 71 | }) 72 | 73 | context('connection error handling', function () { 74 | before(() => nock.disableNetConnect()) 75 | 76 | after(() => nock.enableNetConnect()) 77 | 78 | beforeEach(function () { 79 | this.connection = new Connection(this.defaultOptions) 80 | }) 81 | 82 | it('rejects with connection error if habit is unreachable', function () { 83 | var request = this.connection.get('/user') 84 | 85 | var unknownError = new UnknownConnectionError() 86 | 87 | return expect(request).to.eventually.be.rejected.and.have.property('message', unknownError.message) 88 | }) 89 | 90 | it('uses a custom error handler if provided', function () { 91 | this.connection.setOptions({ 92 | errorHandler: function (err) { 93 | return 'custom error handler for ' + err.name 94 | } 95 | }) 96 | 97 | return this.connection.get('/user').catch((response) => { 98 | expect(response).to.equal('custom error handler for UnknownConnectionError') 99 | }) 100 | }) 101 | }) 102 | 103 | describe('#getOptions', function () { 104 | beforeEach(function () { 105 | this.connection = new Connection(this.defaultOptions) 106 | }) 107 | 108 | it('returns an object with the configured options', function () { 109 | var options = this.connection.getOptions() 110 | 111 | expect(options.id).to.deep.equal(this.defaultOptions.id) 112 | expect(options.apiToken).to.deep.equal(this.defaultOptions.apiToken) 113 | }) 114 | }) 115 | 116 | describe('#setOptions', function () { 117 | beforeEach(function () { 118 | this.connection = new Connection(this.defaultOptions) 119 | }) 120 | 121 | it('sets id after initalization', function () { 122 | expect(this.connection._id).to.equal('myUuid') 123 | 124 | this.connection.setOptions({id: 'newUuid'}) 125 | expect(this.connection._id).to.equal('newUuid') 126 | }) 127 | 128 | it('leaves old id if not passed in after initalization', function () { 129 | expect(this.connection._id).to.equal('myUuid') 130 | 131 | this.connection.setOptions({apiToken: 'foo'}) 132 | expect(this.connection._id).to.equal('myUuid') 133 | }) 134 | 135 | it('leaves old token if not passed in after initalization', function () { 136 | expect(this.connection._apiToken).to.equal('myToken') 137 | 138 | this.connection.setOptions({id: 'foo'}) 139 | expect(this.connection._apiToken).to.equal('myToken') 140 | }) 141 | 142 | it('sets apiToken after initalization', function () { 143 | expect(this.connection._apiToken).to.equal('myToken') 144 | 145 | this.connection.setOptions({apiToken: 'newToken'}) 146 | expect(this.connection._apiToken).to.equal('newToken') 147 | }) 148 | 149 | it('leaves old endpoint if not passed in after initalization', function () { 150 | expect(this.connection._endpoint).to.equal('https://habitica.com') 151 | 152 | this.connection.setOptions({id: 'foo'}) 153 | expect(this.connection._endpoint).to.equal('https://habitica.com') 154 | }) 155 | 156 | it('sets endpoint after initalization', function () { 157 | expect(this.connection._endpoint).to.equal('https://habitica.com') 158 | 159 | this.connection.setOptions({endpoint: 'http://localhost:3321/'}) 160 | expect(this.connection._endpoint).to.equal('http://localhost:3321') 161 | }) 162 | 163 | it('leaves old platform if not passed in after initalization', function () { 164 | expect(this.connection._platform).to.equal('Habitica-Node') 165 | 166 | this.connection.setOptions({id: 'foo'}) 167 | expect(this.connection._platform).to.equal('Habitica-Node') 168 | }) 169 | 170 | it('sets platform after initalization', function () { 171 | expect(this.connection._platform).to.equal('Habitica-Node') 172 | 173 | this.connection.setOptions({platform: 'My Custom Habitica App'}) 174 | expect(this.connection._platform).to.equal('My Custom Habitica App') 175 | }) 176 | }) 177 | 178 | describe('#get', function () { 179 | beforeEach(function () { 180 | this.connection = new Connection(this.defaultOptions) 181 | this.habiticaUrl = nock('https://habitica.com/api/v3').get('/user') 182 | }) 183 | 184 | it('returns a promise', function () { 185 | var request = this.connection.get('/user') 186 | 187 | expect(request).to.be.a('promise') 188 | }) 189 | 190 | it('takes in an optional query parameter', function () { 191 | var expectedRequest = nock('https://habitica.com/api/v3') 192 | .get('/group') 193 | .query({type: 'party'}) 194 | .reply(200) 195 | 196 | return this.connection.get('/group', {query: {type: 'party'}}).then(() => { 197 | expectedRequest.done() 198 | }) 199 | }) 200 | 201 | it('ignores send parameter if passed in', function () { 202 | var expectedRequest = nock('https://habitica.com/api/v3') 203 | .get('/group') 204 | .reply(200) 205 | 206 | return this.connection.get('/group', {send: {type: 'party'}}).then(() => { 207 | expectedRequest.done() 208 | }) 209 | }) 210 | 211 | context('succesful request', function () { 212 | it('returns requested data', function () { 213 | var expectedRequest = this.habiticaUrl.reply(function () { 214 | return [200, { some: 'data' }] 215 | }) 216 | 217 | var connection = new Connection(this.defaultOptions) 218 | return connection.get('/user').then((response) => { 219 | expect(response).to.deep.equal({ some: 'data' }) 220 | expectedRequest.done() 221 | }) 222 | }) 223 | }) 224 | 225 | context('unsuccesful request', function () { 226 | it('passes on error data from API', function () { 227 | var expectedRequest = this.habiticaUrl.reply(function () { 228 | return [404, { 229 | success: false, 230 | error: 'NotFound', 231 | message: 'User not found.' 232 | }] 233 | }) 234 | 235 | var connection = new Connection(this.defaultOptions) 236 | 237 | return connection.get('/user').then(() => { 238 | throw new Error('Expected Rejection') 239 | }).catch((err) => { 240 | expect(err).to.be.an.instanceof(HabiticaApiError) 241 | expect(err.status).to.equal(404) 242 | expect(err.name).to.equal('HabiticaApiNotFoundError') 243 | expect(err.type).to.equal('NotFound') 244 | expect(err.message).to.equal('User not found.') 245 | 246 | expectedRequest.done() 247 | }) 248 | }) 249 | }) 250 | }) 251 | 252 | describe('#post', function () { 253 | beforeEach(function () { 254 | this.habiticaUrl = nock('https://habitica.com/api/v3') 255 | .post('/user/tasks') 256 | }) 257 | 258 | it('returns a promise', function () { 259 | this.habiticaUrl.reply(function () { 260 | return [200, { some: 'data' }] 261 | }) 262 | var connection = new Connection(this.defaultOptions) 263 | var request = connection.post('/user/tasks') 264 | 265 | expect(request).to.be.a('promise') 266 | }) 267 | 268 | it('takes in an optional query parameter', function () { 269 | var expectedRequest = nock('https://habitica.com/api/v3') 270 | .post('/user/tasks') 271 | .query({ 272 | type: 'habit', 273 | text: 'test habit' 274 | }) 275 | .reply(201, {}) 276 | 277 | var connection = new Connection(this.defaultOptions) 278 | return connection.post('/user/tasks', { 279 | query: { 280 | type: 'habit', 281 | text: 'test habit' 282 | } 283 | }).then(() => { 284 | expectedRequest.done() 285 | }) 286 | }) 287 | 288 | it('takes in an optional send parameter', function () { 289 | var expectedRequest = nock('https://habitica.com/api/v3') 290 | .post('/group', { 291 | type: 'party' 292 | }) 293 | .reply(200) 294 | 295 | var connection = new Connection(this.defaultOptions) 296 | return connection.post('/group', {send: {type: 'party'}}).then(() => { 297 | expectedRequest.done() 298 | }) 299 | }) 300 | 301 | context('succesful request', function () { 302 | it('returns requested data', function () { 303 | var expectedRequest = this.habiticaUrl.reply(function () { 304 | return [200, { some: 'data' }] 305 | }) 306 | 307 | var connection = new Connection(this.defaultOptions) 308 | return connection.post('/user/tasks').then((response) => { 309 | expect(response).to.deep.equal({ some: 'data' }) 310 | expectedRequest.done() 311 | }) 312 | }) 313 | }) 314 | 315 | context('unsuccesful request', function () { 316 | it('rejects if credentials are not valid', function () { 317 | var expectedRequest = this.habiticaUrl.reply(function () { 318 | return [401, {response: {status: 401, text: 'Not Authorized'}}] 319 | }) 320 | 321 | var connection = new Connection(this.defaultOptions) 322 | return connection.post('/user/tasks').then(() => { 323 | throw new Error('Expected Rejection') 324 | }).catch((err) => { 325 | expect(err.status).to.equal(401) 326 | expectedRequest.done() 327 | }) 328 | }) 329 | }) 330 | }) 331 | 332 | describe('#put', function () { 333 | beforeEach(function () { 334 | this.habiticaUrl = nock('https://habitica.com/api/v3') 335 | .put('/user/tasks') 336 | }) 337 | 338 | it('returns a promise', function () { 339 | this.habiticaUrl.reply(function () { 340 | return [200, { some: 'data' }] 341 | }) 342 | var connection = new Connection(this.defaultOptions) 343 | var request = connection.put('/user/tasks') 344 | 345 | expect(request).to.be.a('promise') 346 | }) 347 | 348 | it('takes in an optional query parameter', function () { 349 | var expectedRequest = nock('https://habitica.com/api/v3') 350 | .put('/user/tasks') 351 | .query({ 352 | type: 'habit', 353 | text: 'test habit' 354 | }) 355 | .reply(201, {}) 356 | 357 | var connection = new Connection(this.defaultOptions) 358 | return connection.put('/user/tasks', { 359 | query: { 360 | type: 'habit', 361 | text: 'test habit' 362 | } 363 | }).then(() => { 364 | expectedRequest.done() 365 | }) 366 | }) 367 | 368 | it('takes in an optional send parameter', function () { 369 | var expectedRequest = nock('https://habitica.com/api/v3') 370 | .put('/group', { 371 | type: 'party' 372 | }) 373 | .reply(200) 374 | 375 | var connection = new Connection(this.defaultOptions) 376 | return connection.put('/group', {send: {type: 'party'}}).then((response) => { 377 | expectedRequest.done() 378 | }) 379 | }) 380 | 381 | context('succesful request', function () { 382 | it('returns requested data', function () { 383 | var expectedRequest = this.habiticaUrl.reply(function () { 384 | return [200, { some: 'data' }] 385 | }) 386 | 387 | var connection = new Connection(this.defaultOptions) 388 | return connection.put('/user/tasks').then((response) => { 389 | expect(response).to.deep.equal({ some: 'data' }) 390 | expectedRequest.done() 391 | }) 392 | }) 393 | }) 394 | 395 | context('unsuccesful request', function () { 396 | it('rejects if credentials are not valid', function () { 397 | var expectedRequest = this.habiticaUrl.reply(function () { 398 | return [401, {response: {status: 401, text: 'Not Authorized'}}] 399 | }) 400 | 401 | var connection = new Connection(this.defaultOptions) 402 | return expect(connection.put('/user/tasks')).to.eventually.be.rejected.then((response) => { 403 | expectedRequest.done() 404 | }) 405 | }) 406 | }) 407 | }) 408 | 409 | describe('#del', function () { 410 | beforeEach(function () { 411 | this.habiticaUrl = nock('https://habitica.com/api/v3') 412 | .delete('/user/tasks') 413 | }) 414 | 415 | it('returns a promise', function () { 416 | this.habiticaUrl.reply(function () { 417 | return [200, { some: 'data' }] 418 | }) 419 | var connection = new Connection(this.defaultOptions) 420 | var request = connection.del('/user/tasks') 421 | 422 | expect(request).to.be.a('promise') 423 | }) 424 | 425 | it('takes in an optional query parameter', function () { 426 | var expectedRequest = nock('https://habitica.com/api/v3') 427 | .delete('/user/tasks') 428 | .query({ 429 | type: 'habit', 430 | text: 'test habit' 431 | }) 432 | .reply(201, {}) 433 | 434 | var connection = new Connection(this.defaultOptions) 435 | return connection.del('/user/tasks', { 436 | query: { 437 | type: 'habit', 438 | text: 'test habit' 439 | } 440 | }).then(() => { 441 | expectedRequest.done() 442 | }) 443 | }) 444 | 445 | it('takes in an optional send parameter', function () { 446 | var expectedRequest = nock('https://habitica.com/api/v3') 447 | .delete('/group', { 448 | type: 'party' 449 | }) 450 | .reply(200) 451 | 452 | var connection = new Connection(this.defaultOptions) 453 | return connection.del('/group', {send: {type: 'party'}}).then((response) => { 454 | expectedRequest.done() 455 | }) 456 | }) 457 | 458 | context('succesful request', function () { 459 | it('returns requested data', function () { 460 | var expectedRequest = this.habiticaUrl.reply(function () { 461 | return [200, { some: 'data' }] 462 | }) 463 | 464 | var connection = new Connection(this.defaultOptions) 465 | return connection.del('/user/tasks').then((response) => { 466 | expect(response).to.deep.equal({ some: 'data' }) 467 | expectedRequest.done() 468 | }) 469 | }) 470 | }) 471 | 472 | context('unsuccesful request', function () { 473 | it('rejects if credentials are not valid', function () { 474 | var expectedRequest = this.habiticaUrl.reply(function () { 475 | return [401, {response: {status: 401, text: 'Not Authorized'}}] 476 | }) 477 | 478 | var connection = new Connection(this.defaultOptions) 479 | return expect(connection.del('/user/tasks')).to.eventually.be.rejected.then((response) => { 480 | expectedRequest.done() 481 | }) 482 | }) 483 | }) 484 | }) 485 | 486 | describe('delete', function () { 487 | it('is an alias for del', function () { 488 | expect(Connection.prototype.delete).to.equal(Connection.prototype.del) 489 | }) 490 | }) 491 | 492 | describe('_router', function () { 493 | beforeEach(function () { 494 | this.connection = new Connection(this.defaultOptions) 495 | this.habiticaUrl = nock('https://habitica.com') 496 | }) 497 | 498 | var METHODS = ['get', 'post', 'put', 'delete'] 499 | var TOP_LEVEL_ROUTES = ['/logout', '/export', '/email', '/qr-code', '/amazon', '/iap', '/paypal', '/stripe'] 500 | 501 | TOP_LEVEL_ROUTES.forEach((route) => { 502 | METHODS.forEach((method) => { 503 | it('handles top level routes without api/v3 prefix for ' + method + ' request on ' + route, function () { 504 | var expectedRequest = this.habiticaUrl[method](route).reply(function () { 505 | return [200, { some: 'data' }] 506 | }) 507 | 508 | return this.connection[method](route).then((response) => { 509 | expect(response).to.deep.equal({ some: 'data' }) 510 | expectedRequest.done() 511 | }) 512 | }) 513 | 514 | it('handles subpages of top level routes without api/v3 prefix for ' + method + ' request on ' + route, function () { 515 | var expectedRequest = this.habiticaUrl[method](route + '/foo').reply(function () { 516 | return [200, { some: 'data' }] 517 | }) 518 | 519 | return this.connection[method](route + '/foo').then((response) => { 520 | expect(response).to.deep.equal({ some: 'data' }) 521 | expectedRequest.done() 522 | }) 523 | }) 524 | }) 525 | }) 526 | }) 527 | }) 528 | --------------------------------------------------------------------------------