From 97098482afed0c9b9b564d4d05509ccdffa5c38d Mon Sep 17 00:00:00 2001 From: Mohammed Sohail Date: Fri, 25 Mar 2022 13:14:35 +0300 Subject: [PATCH] update: separate stateful and stateless scenarios --- README.md | 36 ++++-- config.js | 13 ++ lib.js | 14 ++- scenarios/reset_pin/reset_pin.test.js | 113 ++++++++++++++++++ .../reset_pin}/tap-parallel-not-ok | 0 .../stateless/display_sarafu.test.js | 17 +-- .../stateless/initial_menu.test.js | 17 +-- scenarios/stateless/tap-parallel-not-ok | 0 yarn.lock | 8 +- 9 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 config.js create mode 100644 scenarios/reset_pin/reset_pin.test.js rename {tests => scenarios/reset_pin}/tap-parallel-not-ok (100%) rename tests/2_display_sarafu.test.js => scenarios/stateless/display_sarafu.test.js (57%) rename tests/1_initial_menu.test.js => scenarios/stateless/initial_menu.test.js (57%) create mode 100644 scenarios/stateless/tap-parallel-not-ok diff --git a/README.md b/README.md index d762e33..0775dfa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # cic-ussd-e2e -> Mocking user interaction with cic-ussd +Bunch of test scripts to automate traversing USSD menu's with little human interaction and as close replication to actual USSD usage including AT style reqests and waiting times. ## Setup @@ -15,19 +15,39 @@ - Node.js - Install dependencies with `$ npm i` +- Set global config in `config.js` + +## Scenarios + +| Test | Type | Description | +| -------------- | --------- | ---------------------------------------------- | +| initial_menu | Statless | Main menu > Help > Exit | +| display_sarafu | Stateless | Main menu > My Sarafu > Exit | +| reset_pin | Stateful | Main menu > Account options > Reset pin > Exit | + +### Stateful scenarios + +These tests live individually in their own folder. They require some ENV variables to be set before running them. + +### Stateless scenarios + +These are included under the `stateless` folder and can be run in any order and have multple tests under the same folder since they only check correctness of menu responses. ## Running tests ```bash -$ npm run test +# Set env variables for stateful scenarios, refer to individual test folders +$ npx tap --bail scenarios/SCENARIO_NAME ``` -## Scenarios +Examples: -| Test | Description | -| ---------------- | ---------------------------- | -| 1_initial_menu | Main menu > Help > Exit | -| 2_display_sarafu | Main menu > My Sarafu > Exit | +- `CURRENT_PIN=4444 NEW_PIN=3333 npx tap --bail scenarios/stateless` + +### Limitations + +- These scripts cannot reset USSD sessions upon failure. You will need to manually set your account USSD menu to the main menu +- Cannot verify sms status, confirm with cic-notify for delivery ### Writing your own scenario @@ -35,6 +55,8 @@ $ npm run test - Control wait between menus with the `wait(time in ms)` function - Follow any spec - For any menu traversal, remember to append a `*` before the next input, this correctly mocks AT webhooks e.g. +- Dyanmic inputs for stateful scenarios must come from an env var that can be set again on next run +- Include a `tap-parallel-not-ok` file in any new folder ```bash # Initial Main menu, no input diff --git a/config.js b/config.js new file mode 100644 index 0000000..0cf92a7 --- /dev/null +++ b/config.js @@ -0,0 +1,13 @@ +module.exports = { + ussd: { + serviceCode: "*483*061#", + endpoint: "https://ussd.grassecon.net", + timeout: 3000, + }, + test: { + waitNextMenu: 3000, + }, + user: { + ussdPhone: "254711777734", + }, +}; diff --git a/lib.js b/lib.js index c973648..aedf384 100644 --- a/lib.js +++ b/lib.js @@ -1,17 +1,19 @@ const crypto = require("crypto"); const phin = require("phin"); -async function ussdClient(sessionId, input = "") { +const conf = require("./config"); + +async function ussdClient(phone, sessionId, input = "") { const requestOptions = { - url: "https://ussd.grassecon.net", + url: conf.ussd.endpoint, method: "POST", parse: "string", - timeout: 3000, + timeout: conf.ussd.timeout, form: { sessionId: sessionId, - // Get from a config file, then pass as a param - phoneNumber: "254711777734", - serviceCode: "*483*061#", + // Get from a conf file, then pass as a param + phoneNumber: phone, + serviceCode: conf.ussd.serviceCode, text: input, }, }; diff --git a/scenarios/reset_pin/reset_pin.test.js b/scenarios/reset_pin/reset_pin.test.js new file mode 100644 index 0000000..4cb4d34 --- /dev/null +++ b/scenarios/reset_pin/reset_pin.test.js @@ -0,0 +1,113 @@ +const test = require("tap").test; +const rangi = require("rangi"); + +const lib = require("../../lib"); +const conf = require("../../config"); + +test("Reset Pin", async (t) => { + t.before((t) => { + if (!process.env.CURRENT_PIN || !process.env.NEW_PIN) { + t.bailout("ENV not set"); + } + }); + + const sessionId = lib.newSession(); + const currentPin = process.env.CURRENT_PIN; + const newPin = process.env.NEW_PIN; + + t.test("Display menu and Sarafu balance", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /Balance/g); + t.end(); + }); + + t.test("Go to My Accont menu", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "3"); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /PIN options/g); + t.end(); + }); + + t.test("Go to Pin options", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "3*5"); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /Change my PIN/g); + t.end(); + }); + + t.test("Start Pin Change", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "3*5*1"); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /Enter current PIN/g); + t.end(); + }); + + t.test("Enter current Pin", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient( + conf.user.ussdPhone, + sessionId, + `3*5*1*${currentPin}` + ); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /new four number PIN/g); + t.end(); + }); + + t.test("Enter new Pin", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient( + conf.user.ussdPhone, + sessionId, + `3*5*1*${currentPin}*${newPin}` + ); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /new four number PIN again/g); + t.end(); + }); + + t.test("Enter new Pin agin", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient( + conf.user.ussdPhone, + sessionId, + `3*5*1*${currentPin}*${newPin}*${newPin}` + ); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "CON"); + t.match(r.text, /Your request has been sent/g); + t.end(); + }); + + t.test("Exit", async (t) => { + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient( + conf.user.ussdPhone, + sessionId, + `3*5*1*${currentPin}*${newPin}*${newPin}*99` + ); + console.log(rangi.cyan(r.text)); + + t.equal(r.code, "END"); + t.match(r.text, /Thank/g); + t.end(); + }); +}); diff --git a/tests/tap-parallel-not-ok b/scenarios/reset_pin/tap-parallel-not-ok similarity index 100% rename from tests/tap-parallel-not-ok rename to scenarios/reset_pin/tap-parallel-not-ok diff --git a/tests/2_display_sarafu.test.js b/scenarios/stateless/display_sarafu.test.js similarity index 57% rename from tests/2_display_sarafu.test.js rename to scenarios/stateless/display_sarafu.test.js index 0648da7..de24320 100644 --- a/tests/2_display_sarafu.test.js +++ b/scenarios/stateless/display_sarafu.test.js @@ -1,16 +1,17 @@ const test = require("tap").test; const rangi = require("rangi"); -const lib = require("../lib"); +const lib = require("../../lib"); +const conf = require("../../config"); -test("Initial menu, display balances, exit", async (t) => { +test("Display Sarafu", async (t) => { const sessionId = lib.newSession(); t.plan(3); t.test("Display menu and Sarafu balance", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId); console.log(rangi.cyan(r.text)); t.equal(r.code, "CON"); @@ -19,8 +20,8 @@ test("Initial menu, display balances, exit", async (t) => { }); t.test("Go to My Sarafu menu", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId, "2"); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "2"); console.log(rangi.cyan(r.text)); t.equal(r.code, "CON"); @@ -29,8 +30,8 @@ test("Initial menu, display balances, exit", async (t) => { }); t.test("Exit", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId, "2*00"); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "2*00"); console.log(rangi.cyan(r.text)); t.equal(r.code, "END"); diff --git a/tests/1_initial_menu.test.js b/scenarios/stateless/initial_menu.test.js similarity index 57% rename from tests/1_initial_menu.test.js rename to scenarios/stateless/initial_menu.test.js index 9ba89d1..1071e2d 100644 --- a/tests/1_initial_menu.test.js +++ b/scenarios/stateless/initial_menu.test.js @@ -1,16 +1,17 @@ const test = require("tap").test; const rangi = require("rangi"); -const lib = require("../lib"); +const lib = require("../../lib"); +const conf = require("../../config"); -test("Initial menu, go to help, exit", async (t) => { +test("Initial Menu", async (t) => { const sessionId = lib.newSession(); t.plan(3); t.test("Display menu and Sarafu balance", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId); console.log(rangi.cyan(r.text)); t.equal(r.code, "CON"); @@ -19,8 +20,8 @@ test("Initial menu, go to help, exit", async (t) => { }); t.test("Go to help menu", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId, "4"); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "4"); console.log(rangi.cyan(r.text)); t.equal(r.code, "CON"); @@ -29,8 +30,8 @@ test("Initial menu, go to help, exit", async (t) => { }); t.test("Exit", async (t) => { - await lib.wait(3000); - const r = await lib.ussdClient(sessionId, "4*99"); + await lib.wait(conf.test.waitNextMenu); + const r = await lib.ussdClient(conf.user.ussdPhone, sessionId, "4*99"); console.log(rangi.cyan(r.text)); t.equal(r.code, "END"); diff --git a/scenarios/stateless/tap-parallel-not-ok b/scenarios/stateless/tap-parallel-not-ok new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index a3ef70d..10db74f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -448,7 +448,7 @@ browserslist@^4.17.5: resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.2.tgz#567b41508757ecd904dab4d1c646c612cd3d4f88" integrity sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA== dependencies: - caniuse-lite "^1.0.30001317" + caniuse-lite "^1.0.config.test.waitNextMenu1317" electron-to-chromium "^1.4.84" escalade "^3.1.1" node-releases "^2.0.2" @@ -493,9 +493,9 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30001317: - version "1.0.30001320" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz#8397391bec389b8ccce328636499b7284ee13285" +caniuse-lite@^1.0.config.test.waitNextMenu1317: + version "1.0.config.test.waitNextMenu1320" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.config.test.waitNextMenu1320.tgz#8397391bec389b8ccce328636499b7284ee13285" integrity sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA== cardinal@^2.1.1: