Cucumber Framework

The TypeScript/Cucumber BDD framework drives end-to-end integration tests against the FloatMe QA environment. Each scenario assembles a synthetic user through a declarative Given/When/Then pipeline, calls real service APIs over SigV4-signed HTTP, and asserts on the responses.

cucumber test flow

Folder Layout

cucumber/
├── features/
│   ├── features/          ← .feature files (imported from Jira/Xray)
│   └── step-definitions/  ← TypeScript step implementations
├── import_features.sh     ← pulls feature files from Xray
├── export_results.sh      ← pushes results back to Xray
├── cucumber.json          ← runner config (parallel, retry, loader)
└── .env                   ← local env overrides (not committed)

World Context

customWorld extends Cucumber’s World class and carries all state that is shared across steps within a single scenario. Every scenario starts with a fresh customWorld instance; nothing persists between scenarios.

Field Type Populated By Purpose

userID

string

First When step (The user is created)

Returned by POST /qa/integration/custom-user; threaded through all subsequent API calls as the path parameter.

loanID

string

Given (pre-seeded loan) or LOC When step (The user creates a loan …)

Identifies the active LOC loan for payment, collection, and assertion steps.

loanApplicationID

string

When (The user starts a loan application …)

Returned by POST /{userID}/loans/applications; required to generate and confirm loan details.

confirmationID

string

When (The user makes a manual payment …)

Returned by POST /{userID}/loans/{loanID}/payback; used when sending a failed-payment event to Kinesis.

paymentAmount

number

When (The user makes a manual payment …)

Stored alongside confirmationID so the Kinesis event step can echo the correct amount back.

aws

AwsClient

Before hook (per-scenario)

aws4fetch client pre-loaded with temporary STS credentials; all service calls use this.aws.fetch() for SigV4 signing.

qaApiUrl

string

Before hook (per-scenario)

Base URL for the QA simulation API (/qa/… and /qa/simulate/… endpoints).

subscriptionsApiUrl

string

Before hook (per-scenario)

Base URL for the Subscription service.

underwritingApiUrl

string

Before hook (per-scenario)

Base URL for the Underwriting service.

locApiUrl

string

Before hook (per-scenario)

Base URL for the Line of Credit service.

paymentsApiUrl

string

Before hook (per-scenario)

Base URL for the Payments service.

userApiUrl

string

Before hook (per-scenario)

Base URL for the User service.

floatApiUrl

string

Before hook (per-scenario)

Base URL for the Float service.

customUser

QAIntegrationUser

Given steps (builder pattern)

Accumulates the full user specification sent to POST /qa/integration/custom-user. Built incrementally before user creation.

cachedSubscriptions

Subscription[]

Then assertion steps in subscriptions.ts

Lazy-loaded from GET /{userID}/subscriptions; reused across multiple Then steps in the same scenario.

cachedMembership

Membership

When (The user’s membership is retrieved) or Then steps in membership.ts

Lazy-loaded from GET /{userID}/user/membership.

cachedUser

User

When (The user is retrieved) or Then steps in membership.ts

Lazy-loaded from GET /users/{userID}.

cachedLoans

LocLoan[]

Then assertion steps and collection When steps in loc.ts

Lazy-loaded from GET /{userID}/loans; explicitly cleared (= undefined) after mutations to force a fresh fetch.

cachedUnderwritingResults

UnderwritingEligibility

Then assertion steps in underwriting.ts

Lazy-loaded from GET /{userID}/underwriting/eligibility.

cachedPayments

Payment[]

When (The users payments are retrieved) or Then steps in payments.ts

Lazy-loaded from GET /{userID}/payments.

cachedFloats

Float[]

When / Then steps in floats.ts

Lazy-loaded from GET /{userID}/floats.

cachedActiveFloats

Float[]

When / Then steps in floats.ts

Lazy-loaded from GET /{userID}/floats/active.

cachedFloatHistory

FloatHistory[]

When (The {string} floats history is retrieved)

Loaded from GET /{userID}/floats/{floatID}/collection-history.

cachedFloatHistories

Record<string, FloatHistory[]>

Then collection-history assertion steps in floats.ts

Collection history keyed by float ID, lazy-loaded per float so assertions can span more than one float. Cleared with the other float caches by every step that changes float state.

floatReactivation

ReactivationPaybackResult

When (The users past due floats are paid back for reactivation)

HTTP status (200 full, 402 partial) plus collected, remaining, and total amounts from POST /{userID}/floats/payback/reactivate.

floatCollectionResults

FloatCollectionJobResult[]

When (Float collections are run)

Per-float job results from POST /qa/simulate/floats/daily; read by Then The {string} float collection job should report skip reason {string} to pin why a job skipped a float.

loanDetailsResponse

LoanDetailsResponse

When (The user generates loan details …)

Holds the proposed installment schedule returned by POST /{userID}/loans/generate_loan_details; referenced by the loan creation step.

lastResponse

Response | undefined

When steps that intentionally allow failure (e.g. float requests)

Raw fetch Response stored for HTTP status code assertion steps (Then The user should receive a {int} status code).

originalInstallmentAmount

number | undefined

When (The loan {int} installment date is updated to be {int} days overdue)

Captures the installment total before a late fee is assessed; used to verify the exact 5% fee calculation.

originalLoanAmount

number | undefined

When (installment backdating step, first call)

Captures the loan total before any late fees are added; used by Then The loan total amount should include late fee.

partialPaymentRemainingBalance

number | undefined

When (The user makes a partial payment …)

Remaining installment balance after a partial payment; available for downstream late-fee calculations.

partialPaymentAmount

number | undefined

When (The user makes a partial payment …)

Amount of the most recent partial payment; available for downstream assertions.

Hooks

Target environment

QA_ENV (default test) is the only per-environment setting. environment.ts derives everything else from it: the Plaid secret is <env>/plaid, and every service URL is https://<application>.<env>.svc.floatme.io, the hostname each service creates in the infrastructure repo’s per-environment zone.

World field Application

qaApiUrl

qa

paymentsApiUrl

payments

subscriptionsApiUrl

subscription-service

underwritingApiUrl

underwriting

locApiUrl

loc-service

userApiUrl

user-service

floatApiUrl

floats

transactionsApiUrl

txn

Credentials

The suite never assumes a role. Credentials and region come from the AWS SDK’s default chain, so whatever starts the suite is responsible for arriving as the environment’s Cucumber runner role, which can invoke the service APIs and read the Plaid secret.

Entry point Runner role How it gets there

CI (cucumber.yaml)

arn:aws:iam::267052520423:role/test-cucumber-qa-role

configure-aws-credentials assumes the role through GitHub OIDC; no static keys

Local

arn:aws:iam::208295047603:role/staging-cucumber-qa-role

make cucumber.staging (devkit) or make cucumber.staging.host points AWS_CONFIG_FILE at cucumber/aws.config, whose cucumber-staging profile assumes the role from a ReadOnlyAccess SSO session in the staging account

Local runs target staging only; the shared test account is outside the organization, so Identity Center cannot reach it. cucumber/aws.config ships with the repo, so nothing under ~/.aws needs editing. When the SSO session has expired, sign in with aws sso login --sso-session floatme (the token cache is shared with any profile using that session name). Keep AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY out of cucumber/.env; a profile is the only credential source.

BeforeAll

Runs once before the entire test suite. Resolves the ambient credentials and region, stores them on this.parameters alongside the computed API URLs, and reads the Plaid sandbox client id and secret from <env>/plaid.

Before (per-scenario)

Runs before each individual scenario. Reads the credentials from this.parameters and constructs an AwsClient (from aws4fetch) with retries: 0, then copies the environment’s API URLs onto the world.

The default step timeout is 60 seconds (setDefaultTimeout(60 * 1000)).

Step Definition Files

File Responsibility Key Steps (representative patterns)

world.ts

Defines the customWorld class with all scenario-level state fields and types.

(no step registrations — world definition only)

hooks.ts

Lifecycle hooks: BeforeAll credential and Plaid secret resolution, Before per-scenario world initialisation.

(no step registrations — hooks only)

environment.ts

Derives the Plaid secret name and every service URL from QA_ENV.

(no step registrations — configuration only)

types.ts

TypeScript interfaces for all domain objects (QAIntegrationUser, LocLoan, Installment, Payment, Subscription, etc.) and the companies constant array.

(no step registrations — type declarations only)

helpers.ts

Pure utility functions shared across step files: date formatting, relative-date parsing, ordinal-to-index conversion, random string/account generation.

toISODate(), getDateFromRelative(), getPosition(), getRandomAccountNumber()

user.ts

Given steps that build customUser; the primary When step that creates the user via the QA API. Exports createUser() reused by other files.

Given A user
Given They have a balance of {int}
When The user is created

floats.ts

When steps that request floats or trigger float collections; Then steps that assert float count, status, amount, fee, type, and collection history.

When The user requests a(n) {string} ${int} float
When Float collections are run
Then The {string} float status should be {string}

subscriptions.ts

When steps that trigger subscription collections or pay-ahead; Then steps that assert subscription count, status, error, and amount.

When Subscription collections are run
When The user pays their subscription ahead
Then The {string} subscription status should be {string}

loc.ts

Given steps for pre-seeded loans; When steps for the full LOC application flow, manual payments, partial/full payments, installment date manipulation, and collections triggers; Then steps for loan state, installment status, late fees, and Iterable events.

When The user applies for a loan of {int} with autopay {string} and late fees {string}
When The LOC collections process {word} is triggered with {int} look ahead days
Then The {int} installment should have late fee assessed

membership.ts

When steps for all membership lifecycle actions (upgrade, downgrade, cancel, reactivate, pause, unpause, retract); Then steps that assert user status, membership tier, and membership status.

When The user upgrades to {string} membership
When The user cancels their membership
Then The users membership tier should be {string}

underwriting.ts

When step to trigger an underwriting recalculation; Then step to assert float approval.

When Underwriting runs for the user
Then The user should be approved to float

payments.ts

When steps to retrieve payments or add a debit card via the Payments API; Then steps to assert payment count, status, and amount by payment type (subscription, float, proration).

When The users payments are retrieved
Given The user adds a new debit card
Then The user should have {int} {string} payment(s)

Given / When / Then Conventions

Given Steps — customUser Builder

Every Given step mutates this.customUser — the QAIntegrationUser object that will be POSTed to the QA API. The first Given step (Given A user) initialises the object with faker-generated defaults; all subsequent Given steps add or override specific fields. No network calls are made during Given steps.

Example: building a user with income and a Plaid account
Given A user
And They have a connected plaid account
And They have valid income
And They have a valid debit card
And They have a "standard" "PENDING" float due "5 days from now" for amount 50 and fee 5

The accumulated customUser payload includes nested objects for user, plaid.override_accounts, floats, subscriptions, loans, income, and debit_card.

When Steps — First When Creates the User

The first When step in nearly every scenario calls createUser(), which POSTs the fully assembled customUser to POST /qa/integration/custom-user. The response returns a user_id stored on this.userID. A 10-second sleep follows to allow downstream async processing to complete.

When The user is created

Subsequent When steps call simulation or service endpoints using the now-populated this.userID:

  • POST /qa/simulate/subscriptions/daily — triggers the subscription collections batch for the user

  • POST /qa/simulate/floats/daily — runs the Float Service collection jobs for the user’s collectable floats synchronously and returns per-float results

  • POST /qa/simulate/loc/collections — triggers LOC collections (with process and look_ahead_days params)

  • POST /{userID}/loans/applications → POST /{userID}/loans/generate_loan_details → POST /{userID}/loans — three-step LOC application flow

  • POST /{userID}/loans/{loanID}/payback — manual loan payment

  • POST /{userID}/user/membership/upgrade|downgrade|cancel|pause|unpause — membership lifecycle actions

Then Steps — Assertion Patterns

Then steps call service read APIs directly with this.aws.fetch() (SigV4-signed) and use Chai expect or assert for assertions. Responses are cached on the world object using the nullish-assignment pattern (??=) so multiple Then steps in the same scenario share one network round-trip.

Example: asserting float state after collections
Then The user should have 1 float(s)
And The "first" float status should be "COMPLETED"
And The "first" float amount should be 50

Error-path assertions use this.lastResponse (stored by steps that intentionally suppress throw):

Then The user should receive a 400 status code

Parallelism and Retry

Configuration lives in cucumber/cucumber.json:

{
    "default": {
        "loader": ["ts-node/esm"],
        "import": ["features/step-definitions/**/*.ts"],
        "parallel": 5,
        "retry": 1,
        "strict": false
    }
}
  • parallel: 5 — up to 5 scenarios run concurrently using Cucumber’s worker pool. Each worker gets its own customWorld instance and its own Before hook invocation (separate AwsClient).

  • retry: 1 — a failing scenario is automatically retried once before being marked failed. This tolerates transient API latency or async processing delays.

  • strict: false — undefined or pending steps are reported but do not abort the run.

The test runner is invoked with:

yarn test   # expands to: cucumber-js -f json:out.json

A local run targets staging through the shipped AWS config (see Credentials):

make cucumber.staging ARGS='--tags @ENG-11943'

Results are written to out.json for Xray import.

Tagging Conventions

Each .feature file carries a feature-level tag matching its Jira issue key (e.g. @ENG-8820). Individual scenarios may carry additional tags for grouping or filtering.

  • import_features.sh — pulls updated .feature files from Xray using the issue keys embedded in file names.

  • export_results.sh — reads out.json after a run and pushes results back to Xray, matching results to test executions by tag.

Running a single suite by tag:

cucumber-js --tags @ENG-9692

Test Suites

Feature File Focus Area Jira Key

ENG-8820_Subscription_Collections.feature

Subscription billing: COMPLETED, FAILED, RETRIED, error codes, pay-ahead flows

ENG-8820

ENG-9250_Underwriting_Cucumber.feature

Underwriting eligibility: income detection, balance thresholds, float approval/denial

ENG-9250

ENG-9692_LOC_Cucumber_Test_Set.feature

Line of Credit: loan origination, collections, manual payments, late fees, Iterable events

ENG-9692

ENG-11544_Users_and_Membership.feature

User lifecycle and membership tier management: upgrade, downgrade, cancel, pause, reactivate

ENG-11544

ENG-11943_Floats.feature

Float requests, status assertions, active/historical float counts

ENG-11943

ENG-12341_Float_Collections.feature

Float collection jobs (initial, daily retry, day-before ACH, prenotes): outcome, payment, and collection-history verification

ENG-12341