Skip to content

Commit

Permalink
Update to use Jest tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcL committed Mar 8, 2020
1 parent 14f79b2 commit 5d57daa
Show file tree
Hide file tree
Showing 11 changed files with 1,949 additions and 117 deletions.
6 changes: 4 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"env": {
"es6": true,
"node": true,
"mocha": true
}
"mocha": true,
"jest/globals": true
},
"plugins": ["jest"]
}
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
src/
examples/
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![Build Status](https://travis-ci.org/MarcL/chatfuel-broadcast.svg?branch=master)](https://travis-ci.org/MarcL/chatfuel-broadcast)

A simplified client for using the [Chatfuel broadcast API](http://docs.chatfuel.com/broadcasting/broadcasting-documentation/broadcasting-api).
A simplified client for using the [Chatfuel broadcast API](http://docs.chatfuel.com/broadcasting/broadcasting-documentation/broadcasting-api) which includes rate limiting.

## Installation

Expand Down Expand Up @@ -36,6 +36,8 @@ Create an options object which contains the mandatory parameters of `botId`, `to

Facebook message tags are now mandatory and an error will be thrown if one isn't passed. See [Facebook's message tags](https://developers.facebook.com/docs/messenger-platform/send-messages/message-tags) documentation for a list of valid tags.

_**Note:** In addition to a the valid Facebook Messenger tags, there is a single Chatfuel-specific tag called `UPDATE`. You can **ONLY** use this tag with the broadcast API to send a message if you have recieved an interaction from your user within the previous 24 hours. Your chatbot will get banned if you fail to adhere to this rule._

Add in the attributes property for the Chatfuel user attributes you want to set:

```javascript
Expand Down Expand Up @@ -73,6 +75,45 @@ chatfuelBroadcast(options)
});
```

## TODO: Update response from Chatfuel

Success: (200)

```json
{
"result": "ok",
"success": true
}
```

Failure: (400)
```json
{
"result": "Bad Request: Message tag UPDATE2 is invalid",
"success": false,
"errors": [
"Bad Request: Message tag UPDATE2 is invalid"
],
"message": null
}
```

Rate limited: (429)
```json
{
"result": "Too many requests: RPS limit reached",
"success": false,
"errors": [
"Too many requests: RPS limit reached"
],
"message": null
}
```

## Rate limiting

The Chatfuel documentation states that you can broadcast up to 25 request per second (RPS) using their [broadcast API](https://docs.chatfuel.com/en/articles/790461-broadcasting-api). This package sets this rate limit for you and avoids you having to add additional rate limiting logic in your own code.

## Running the tests

The unit tests can be run using `npm` or `yarn`:
Expand Down
24 changes: 24 additions & 0 deletions examples/excessiveRequests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const broadcast = require('../lib/broadcast');

// Pass through botId, token and user id as environment variables
const {
BOT_ID: botId,
TOKEN: token,
USER_ID: userId,
} = process.env;

const broadcastOptions = {
botId,
token,
userId,
messageTag: 'UPDATE',

blockName: 'time.test',
};

// Try and send more than 25 requests in a second
for (let i = 0; i < 30; i += 1) {
broadcast(broadcastOptions)
.then((data) => console.log(data))
.catch((data) => console.log(data));
}
188 changes: 188 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// For a detailed explanation regarding each configuration property, visit:
// https://jestjs.io/docs/en/configuration.html

module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,

// Stop running tests after `n` failures
// bail: 0,

// Respect "browser" field in package.json when resolving modules
// browser: false,

// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/ht/lvz630p97_q64zh90cspcmh83_ggwr/T/jest_1uh6zc",

// Automatically clear mock calls and instances between every test
clearMocks: true,

// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,

// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,

// The directory where Jest should output its coverage files
coverageDirectory: "coverage",

// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],

// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],

// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,

// A path to a custom dependency extractor
// dependencyExtractor: undefined,

// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,

// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],

// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,

// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,

// A set of global variables that need to be available in all test environments
// globals: {},

// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",

// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],

// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],

// A map from regular expressions to module names that allow to stub out resources with a single module
// moduleNameMapper: {},

// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],

// Activates notifications for test results
// notify: false,

// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",

// A preset that is used as a base for Jest's configuration
// preset: undefined,

// Run tests from one or more projects
// projects: undefined,

// Use this configuration option to add custom reporters to Jest
// reporters: undefined,

// Automatically reset mock state between every test
// resetMocks: false,

// Reset the module registry before running each individual test
// resetModules: false,

// A path to a custom resolver
// resolver: undefined,

// Automatically restore mock state between every test
// restoreMocks: false,

// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,

// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],

// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",

// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],

// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],

// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],

// The test environment that will be used for testing
testEnvironment: "node",

// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},

// Adds a location field to test results
// testLocationInResults: false,

// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],

// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],

// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],

// This option allows the use of a custom results processor
// testResultsProcessor: undefined,

// This option allows use of a custom test runner
// testRunner: "jasmine2",

// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
// testURL: "http://localhost",

// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",

// A map from regular expressions to paths to transformers
// transform: undefined,

// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/"
// ],

// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,

// Indicates whether each individual test should be reported during the run
// verbose: undefined,

// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],

// Whether to use watchman for file crawling
// watchman: true,
};
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chatfuel-broadcast",
"version": "2.2.0",
"version": "3.0.0",
"description": "Wrapper for Chatfuel broadcast API",
"main": "lib/broadcast.js",
"repository": "git@github.com:MarcL/chatfuel-broadcast.git",
Expand All @@ -11,10 +11,11 @@
"build": "babel src -d lib",
"lint": "eslint src test",
"prepublish": "yarn lint && yarn test && yarn build",
"test": "mocha"
"test": "jest"
},
"dependencies": {
"axios": "^0.19.2",
"axios-rate-limit": "^1.2.1",
"is-hex": "^1.1.3",
"request": "^2.88.0",
"request-promise": "^4.2.4"
Expand All @@ -28,7 +29,9 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.18.1",
"eslint-plugin-jest": "^23.8.2",
"husky": "^4.2.3",
"jest": "^25.1.0",
"mocha": "^7.1.0",
"sinon": "^9.0.0"
},
Expand Down
26 changes: 7 additions & 19 deletions src/broadcast.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import isHex from 'is-hex';
import requestPromise from 'request-promise';
import axios from 'axios';
import url from 'url';
import validateFacebookTags from './validateFacebookTags';
import httpClient from './httpClient';

// http://docs.chatfuel.com/broadcasting/broadcasting-documentation/broadcasting-api

const CHATFUEL_BASE_URL = 'https://api.chatfuel.com';
const CHATFUEL_API_RATE_LIMIT_REQUESTS_PER_SECOND = 25;

const validateExpectedParameters = (options) => {
const expectedParameters = ['botId', 'token', 'userId', 'messageTag'];
Expand Down Expand Up @@ -47,19 +45,7 @@ const getBlockIdOrNameFromOptions = (options) => {

const createChatfuelBroadcastUrl = (botId, userId) => `${CHATFUEL_BASE_URL}/bots/${botId}/users/${userId}/send`;

const makeRequest = (uri) => {
const requestOptions = {
uri,
headers: {
'Content-Type': 'application/json',
},
json: true,
};

return requestPromise.post(requestOptions);
};

const makeAxiosRequest = (requestUrl) => {
const makeHttpRequest = (requestUrl) => {
const requestOptions = {
method: 'post',
url: requestUrl,
Expand All @@ -68,7 +54,9 @@ const makeAxiosRequest = (requestUrl) => {
},
};

return axios(requestOptions);
return httpClient(requestOptions)
.then((response) => response.data.result)
.catch((error) => error.response.data.result);
};

const broadcast = (options) => {
Expand All @@ -81,7 +69,7 @@ const broadcast = (options) => {
const chatfuelRedirectBlock = getBlockIdOrNameFromOptions(options);

if (!validateFacebookTags(messageTag)) {
throw new Error(`Invalid Facebook message tag '${messageTag}'`);
throw new Error(`Invalid Facebook or Chatfuel message tag '${messageTag}'`);
}

const chatfuelBroadcastUrl = createChatfuelBroadcastUrl(botId, userId);
Expand All @@ -99,7 +87,7 @@ const broadcast = (options) => {
query,
});

return makeRequest(chatfuelApiUrl);
return makeHttpRequest(chatfuelApiUrl);
};

module.exports = broadcast;
10 changes: 10 additions & 0 deletions src/httpClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import axios from 'axios';
import rateLimit from 'axios-rate-limit';

const CHATFUEL_API_RATE_LIMIT_REQUESTS_PER_SECOND = 25;

const rateLimitingOptions = {
maxRPS: CHATFUEL_API_RATE_LIMIT_REQUESTS_PER_SECOND,
};

module.exports = rateLimit(axios.create(), rateLimitingOptions);

0 comments on commit 5d57daa

Please sign in to comment.