diff --git a/functional_tests/README.md b/functional_tests/README.md index 1c972b2..870ac6e 100644 --- a/functional_tests/README.md +++ b/functional_tests/README.md @@ -65,3 +65,20 @@ --- +**Execution Date:** 4/28/2026, 6:03:40 AM + +**Test Unique Identifier:** "functional-test-generation" + +**Input(s):** + 1. Aegis_WebCC_SRS.pdf + Path: /var/tmp/Roost/RoostGPT/functional-test-generation/712daf43-8ed8-4c93-b878-021d857b68ce/Aegis_WebCC_SRS.pdf + +**Test Output Folder:** + 1. [functional-test-generation.json](functional-test-generation/functional-test-generation.json) + 2. [functional-test-generation.feature](functional-test-generation/functional-test-generation.feature) + 3. [functional-test-generation.csv](functional-test-generation/functional-test-generation.csv) + 4. [functional-test-generation.xlsx](functional-test-generation/functional-test-generation.xlsx) + 5. [functional-test-generation.docx](functional-test-generation/functional-test-generation.docx) + +--- + diff --git a/functional_tests/functional-test-generation/.roost/roost_metadata.json b/functional_tests/functional-test-generation/.roost/roost_metadata.json new file mode 100644 index 0000000..3f0d102 --- /dev/null +++ b/functional_tests/functional-test-generation/.roost/roost_metadata.json @@ -0,0 +1,24 @@ +{ + "project": { + "name": "functional-test-generation", + "created_at": "2026-04-28T06:03:40.059Z", + "updated_at": "2026-04-28T06:03:40.059Z" + }, + "files": { + "input_files": [ + { + "fileName": "functional-test-generation.txt", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-generation/712daf43-8ed8-4c93-b878-021d857b68ce/functional_tests/functional-test-generation/functional-test-generation.txt", + "fileSha": "cf83e1357e" + }, + { + "fileName": "Aegis_WebCC_SRS.pdf", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-generation/712daf43-8ed8-4c93-b878-021d857b68ce/functional_tests/functional-test-generation/Aegis_WebCC_SRS.pdf", + "fileSha": "dcebdb1a12" + } + ] + }, + "api_files": { + "input_files": [] + } +} \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.csv b/functional_tests/functional-test-generation/functional-test-generation.csv new file mode 100644 index 0000000..9e1f3e4 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.csv @@ -0,0 +1,33 @@ +Approved end-to-end: Registration to rescind with card controls, FX transaction, payments, notifications, and rescind window +Application session expiration with save/resume and Step 3 signature validation +Application decision boundaries for FICO thresholds +Registration invalid field validations and weak password handling +Registration valid then duplicate email rejection +Transactions validation errors and CSRF enforcement +Essential buffer boundary and FX fee precision +Transaction frequency rate limiting with step-up MFA retry +Cross-account/card owner-only enforcement returns 403 with no leakage +Owner can access own summary while non-owner cannot +Notifications webhook invalid inputs and authorization checks +Notifications webhook idempotency scoping per account and across channels +Notifications webhook message length and channel-scoped idempotency per account +Email verification lifecycle with blocked pre-verification login and resend +Refresh token rotation under multi-tab concurrency with CSRF continuity +Transactions history category filters, date-range validation, and paging +Login lockout, per-IP rate limiting, refresh rotation, CSRF cross-site protection, inactivity timeout, and PAN masking +Authorized subscription to own account, forbidden cross-account, schema validation, reconnect and dedupe +WebSocket unauthorized handshake and subscribe/unsubscribe lifecycle +Payment scheduling boundaries: min amount, past-date rejection, same-day immediate, and FULL_BALANCE +MINIMUM and STATEMENT_BALANCE payments and precision enforcement +Report lost/stolen irreversible flow, OTP failures, invalid transitions, and PIN format enforcement +Report lost/stolen with delivery address override validation and replacement confirmation +Refresh token TTL expiry, 401 on refresh, re-authentication, and CSRF continuity +Summary include_rewards toggle, rewards floor verification, and owner-only enforcement +Step 2 idempotency and cross-application session token misuse +CSRF token binding and invalid-token rejection across endpoints +Essential over-limit buffer lifecycle with recovery after payment +Transactions maximum amount and FX exchange_rate precision with REQ-006 rounding +Trusted device remember_me 30-day TTL and MFA suppression on known device +Rescind on exact Day 14 with CSRF enforcement and post-closure behavior +Ensure no PII leakage on error payloads and UI across modules +Draft auto-save at 60s, sanitized localStorage, resume, submit clears draft, and inactivity warning \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.docx b/functional_tests/functional-test-generation/functional-test-generation.docx new file mode 100644 index 0000000..13df32b Binary files /dev/null and b/functional_tests/functional-test-generation/functional-test-generation.docx differ diff --git a/functional_tests/functional-test-generation/functional-test-generation.feature b/functional_tests/functional-test-generation/functional-test-generation.feature new file mode 100644 index 0000000..4d2d074 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.feature @@ -0,0 +1,950 @@ +Feature: Aegis Card Platform - End-to-End, Security, Transactions, Payments, Notifications, WebSocket, and Compliance + + Background: + Given the API base URL is 'https://api.aegiscard.com/v2' + And the Portal URL is 'https://portal.aegiscard.com' + And the WebSocket URL is 'wss://realtime.aegiscard.com/v2/stream' + And the Content-Type header is 'application/json' + And cookies are expected to be HttpOnly, Secure with SameSite=Strict + And all times are considered in UTC + + # End-to-End Happy Path and Major Workflows + @api @ui @e2e @AEGIS-E2E-001 + Scenario: Approved end-to-end: Registration to rescind with card controls, FX transaction, payments, notifications, and rescind window + Given I am on the registration page at 'https://portal.aegiscard.com/register' and a CSRF token is loaded + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"Alice", + "last_name":"Reid", + "email":"alice.qa+20260428@example.com", + "password":"S0mething-Str0ng!V3ry", + "date_of_birth":"1998-04-28", + "phone_number":"+14165550123", + "ssn_last4":"1234", + "agree_terms":true + } + """ + Then the response status should be 201 + And I should see 'Verification email sent' in the UI + And no PAN appears in the DOM or network responses + + Given email verification is simulated complete via internal harness + When I send a POST request to '/auth/login' with JSON payload + """ + { + "email":"alice.qa+20260428@example.com", + "password":"S0mething-Str0ng!V3ry", + "mfa_code":"123456", + "device_id":"11111111-1111-4111-8111-111111111111", + "remember_me":true + } + """ + Then the response status should be 200 + And JWT cookies should be set as HttpOnly, Secure with SameSite=Strict + And no tokens should be present in localStorage or sessionStorage + + When I send a POST request to '/auth/token/refresh' with current refresh_token cookie + Then the response status should be 200 + And a new refresh_token should be issued and rotated + When I immediately retry POST '/auth/token/refresh' using the old refresh_token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + + When I send a POST request to '/applications/start' with JSON payload and X-CSRF-Token + """ + { + "full_legal_name":"Alice Marie Reid", + "email":"alice.qa+20260428@example.com", + "phone_number":"+14165550123", + "residential_address":{ + "street":"100 King St W", + "city":"Toronto", + "province":"ON", + "postal_code":"M5H 1J9" + }, + "id_type":"DRIVERS_LICENSE", + "id_number":"R3ID12345" + } + """ + Then the response status should be 201 + And the response JSON should contain 'application_id' and 'session_token' + And the UI should show 'Resume later' link and auto-save drafts every 60s + + When I send a POST request to '/applications/{application_id}/financials' with headers X-App-Session and X-CSRF-Token and JSON payload + """ + { + "employment_status":"EMPLOYED", + "employer_name":"Aegis QA Labs", + "gross_annual_income":95000.00, + "monthly_rent":1800.00, + "existing_debt_payments":250.00, + "sin_consent":true + } + """ + Then the response status should be 200 + And the response JSON field 'status' should equal 'PENDING_REVIEW' + And the response should contain 'fico_pull_id' + + When I send a POST request to '/applications/{application_id}/submit' with X-CSRF-Token and JSON payload + """ + { + "card_product_id":"AEGIS_GOLD", + "e_signature":"QWxpY2UgTSBSZWlk" + } + """ + Then the response status should be 200 + And the decision should be 'APPROVED' + And the response should include 'credit_limit' and 'card_number_masked' matching '**** **** **** 1234' + And no full PAN should appear in DOM or API responses + And an audit log entry should exist for credit_limit change with user_id, session_id, ip_address, timestamp_utc + + When I send a PATCH request to '/cards/{card_id}/status' with X-CSRF-Token and JSON payload + """ + { "status":"Frozen","reason":"Traveling","confirm_otp":"123456" } + """ + Then the response status should be 200 + And the response JSON field 'new_status' should equal 'Frozen' + When I send a PATCH request to '/cards/{card_id}/status' with X-CSRF-Token and JSON payload + """ + { "status":"Active","reason":"Back from travel","confirm_otp":"123456" } + """ + Then the response status should be 200 + And an audit log should include freeze/unfreeze entries + + When I send a PUT request to '/cards/{card_id}/pin' with X-CSRF-Token and JSON payload + """ + { "new_pin":"2580","confirm_pin":"2580","session_otp":"123456" } + """ + Then the response status should be 200 + And the response should include 'updated_at' + And the PIN should not be echoed in UI or logs + + When I send a POST request to '/accounts/{account_id}/transactions' with X-CSRF-Token and JSON payload + """ + { + "transaction_amount":150.00, + "merchant_name":"Maple Books", + "merchant_id":"MB123", + "mcc_code":"5942", + "transaction_type":"PURCHASE", + "description":"Books" + } + """ + Then the response status should be 200 + And the response should include 'transaction_id' and 'auth_code' + + When I send a POST request to '/accounts/{account_id}/transactions' with X-CSRF-Token and JSON payload + """ + { + "transaction_amount":100.00, + "merchant_name":"Hotel Euro", + "merchant_id":"HTL4722", + "mcc_code":"4722", + "currency_code":"USD", + "exchange_rate":1.250000, + "transaction_type":"PURCHASE", + "description":"Hotel" + } + """ + Then the response status should be 200 + And the response should include 'foreign_fee_amount' equal to 3.75 and 'total_cad' equal to 128.75 + + When I perform 10 approved micro-transactions of $1.00 within 60 minutes + And I attempt an 11th transaction of $1.00 + Then the response status should be 429 + And the error code should be 'FREQ_EXCEEDED' + And the response should indicate 'mfa_required' is true + When I retry the 11th transaction with step-up MFA satisfied + Then the response status should be 200 + + When I send a GET request to '/accounts/{account_id}/transactions?from_date={cycle_start}&to_date={cycle_end}&page=1&per_page=25&category=PURCHASE' + Then the response status should be 200 + And only 'PURCHASE' category transactions should be returned + When I send a GET request to '/accounts/{account_id}/transactions?from_date={cycle_start}&to_date={cycle_end}&page=999&per_page=25&category=PURCHASE' + Then the response status should be 200 + And an empty list should be returned with valid pagination metadata + + When I send a GET request to '/accounts/{account_id}/summary?include_rewards=true' + Then the response status should be 200 + And the response should include 'current_balance','available_credit','credit_limit','account_status','billing_cycle_end','points_balance' + And rewards for MCC 4722 should use floor(amount×3) on CAD-converted amount + + When I send a POST request to '/accounts/{account_id}/payments' with X-CSRF-Token and JSON payload + """ + { "payment_amount":200.00,"payment_type":"CUSTOM","bank_account_id":"BANK123" } + """ + Then the response status should be 200 + And the response should include 'new_balance_estimate' + + When I send a POST request to '/accounts/{account_id}/payments' with X-CSRF-Token and JSON payload + """ + { "payment_amount":100.00,"payment_type":"CUSTOM","bank_account_id":"BANK123","scheduled_date":"{+10d}" } + """ + Then the response status should be 200 + And the response should echo the scheduled_date + + When I send a POST request to '/notifications/webhook' with service authorization and JSON payload + """ + { + "account_id":"{account_id}", + "alert_type":"STATEMENT_READY", + "channel":"IN_APP", + "severity":"INFO", + "message_body":"Your statement is ready", + "idempotency_key":"11111111-2222-4333-8444-555555555555" + } + """ + Then the response status should be 200 + When I resend the same webhook payload with the same idempotency_key + Then the response status should be 409 + And the error code should be 'DUPLICATE_NOTIFICATION' + And a single in-app toast should appear + + When I send a DELETE request to '/accounts/{account_id}' with X-CSRF-Token + Then the response status should be 200 or 204 + And subsequent UI and API should reflect account_status 'Closed' + And card access should be disabled + And an immutable audit trail should record the rescind action + And no full PAN appears in any response or UI element + + @api @ui @AEGIS-E2E-002 + Scenario: Application session expiration with save/resume and Step 3 signature validation + Given I have registered and logged in as 'Bob' with MFA and have a CSRF token + When I send a POST request to '/applications/start' with valid Step 1 JSON and receive application_id and session_token + Then a draft should be present in localStorage and UI should show 'Resume later' + When I wait more than 30 minutes to expire X-App-Session + And I send a POST request to '/applications/{application_id}/financials' with expired X-App-Session + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When I use the UI to 'Resume Application' and restart Step 1 + Then a new application_id and session_token should be issued with 201 + When I send a POST request to '/applications/{application_id}/financials' with valid data and X-App-Session + Then the response status should be 200 + And the response should indicate 'PENDING_REVIEW' + When I send a POST request to '/applications/{application_id}/submit' without e_signature + Then the response status should be 400 + And the error code should be 'SIGNATURE_REQUIRED' + When I resubmit Step 3 with a valid e_signature + Then for Bob (FICO 681) the decision should be 'APPROVED' with masked PAN only + + @api @AEGIS-E2E-002 + Scenario Outline: Application decision boundaries for FICO thresholds + Given an authenticated user '' with configured FICO '' + And a new application_id and session_token are obtained via POST '/applications/start' + When I send POST '/applications/{application_id}/financials' with valid data and X-App-Session + Then the response status should be 200 and status 'PENDING_REVIEW' + When I send POST '/applications/{application_id}/submit' with valid e_signature + Then the response status should be 200 + And the decision should be '' + And masked PAN should be present only for APPROVED + And audit trail for credit_limit should exist only if decision is APPROVED + + Examples: + | user_email | fico | expected_decision | + | bob.qa+20260428@example.com | 681 | APPROVED | + | cara.qa+20260428@example.com | 680 | PENDING | + | dan.qa+20260428@example.com | 600 | PENDING | + | eve.qa+20260428@example.com | 599 | DECLINED | + + # Registration Validation + @api @AEGIS-AUTH-REG-007 + Scenario Outline: Registration invalid field validations and weak password handling + Given I am on the registration page and have a CSRF token + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"", + "last_name":"Rivera", + "email":"", + "password":"", + "date_of_birth":"", + "phone_number":"", + "ssn_last4":"", + "agree_terms": + } + """ + Then the response status should be + And the error code should be '' + And no PAN or unmasked SSN should be present in the response + + Examples: + | first_name | email | password | dob | phone | ssn_last4 | agree_terms | status | error_code | + | Alex | user@@example..com | S0mething-Str0ng!V3ry | 2008-04-29 | +14165551234 | 1234 | true | 400 | INVALID_EMAIL | + | Alex | new.qa+20260428@example.com | Short1! | 1990-04-28 | +14165551234 | 1234 | true | 422 | WEAK_PASSWORD | + | Alex | new.qa+20260428@example.com | S0mething-Str0ng!V3ry | 2009-04-29 | +14165551234 | 1234 | true | 400 | DOB_UNDER_18 | + | Alex | new.qa+20260428@example.com | S0mething-Str0ng!V3ry | 1990-04-28 | 4165551234 | 1234 | true | 400 | INVALID_PHONE | + | Alex | new.qa+20260428@example.com | S0mething-Str0ng!V3ry | 1990-04-28 | +14165551234 | 12A4 | true | 400 | INVALID_SSN_LAST4 | + | Alex | new.qa+20260428@example.com | S0mething-Str0ng!V3ry | 1990-04-28 | +14165551234 | 123 | true | 400 | INVALID_SSN_LAST4 | + | Alex | new.qa+20260428@example.com | S0mething-Str0ng!V3ry | 1990-04-28 | +14165551234 | 1234 | false | 400 | TERMS_REQUIRED | + + @api @AEGIS-AUTH-REG-007 + Scenario: Registration valid then duplicate email rejection + Given I am on the registration page and have a CSRF token + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"Alex", + "last_name":"Rivera", + "email":"new.qa+20260428@example.com", + "password":"V3ry-Str0ng!Pass", + "date_of_birth":"2008-04-28", + "phone_number":"+14165551234", + "ssn_last4":"1234", + "agree_terms":true + } + """ + Then the response status should be 201 + And the response should include 'user_id' + When I resend the same POST to '/auth/register' with the same email + Then the response status should be 409 + And the error code should be 'EMAIL_EXISTS' + + # Transactions Validation and Business Rules + @api @AEGIS-TXN-VAL-010 @AEGIS-TXN-003 + Scenario Outline: Transactions validation errors and CSRF enforcement + Given I am authenticated with JWT cookies and have X-CSRF-Token '' + When I send a POST request to '/accounts/{account_id}/transactions' with JSON payload + """ + { + "transaction_amount":, + "merchant_name":"", + "merchant_id":"", + "mcc_code":"", + "currency_code":"", + "exchange_rate":, + "transaction_type":"", + "description":"" + } + """ + Then the response status should be + And the error code should be '' + + Examples: + | csrf_header | amount | merchant_name | merchant_id | mcc | currency | exchange_rate | type | description | status | error_code | + | MISSING | 10.00 | Test | TST1 | 5942 | CAD | null | PURCHASE | CSRF missing | 403 | CSRF_MISSING | + | PRESENT | 1051.00 | OverNonEss | BOOKS1 | 5942 | CAD | null | PURCHASE | Over non-ess | 402 | INSUFFICIENT_FUNDS | + | PRESENT | 0.00 | ZeroAmt | ZERO | 5942 | CAD | null | PURCHASE | Zero amount | 422 | INVALID_AMOUNT | + | PRESENT | -10.00 | NegAmt | NEG | 5942 | CAD | null | PURCHASE | Negative amount | 422 | INVALID_AMOUNT | + | PRESENT | 100.00 | FXMissing | FXM1 | 4722 | USD | null | PURCHASE | FX missing | 400 | EXCHANGE_RATE_REQD | + | PRESENT | 10.00 | BadMCC | BADMCC | 123 | CAD | null | PURCHASE | Bad MCC | 400 | INVALID_MCC | + | PRESENT | 10.00 | LongMerchant | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA A | 5942 | CAD | null | PURCHASE | Long merchant | 400 | INVALID_MERCHANT_ID | + + @api @AEGIS-TXN-VAL-010 + Scenario Outline: Essential buffer boundary and FX fee precision + Given I am authenticated with JWT cookies and have a valid X-CSRF-Token + When I send a POST request to '/accounts/{account_id}/transactions' with JSON payload + """ + { + "transaction_amount":, + "merchant_name":"", + "merchant_id":"", + "mcc_code":"", + "currency_code":"", + "exchange_rate":, + "transaction_type":"PURCHASE", + "description":"" + } + """ + Then the response status should be + And the field '' should equal '' + And if '' is 'USD' then 'foreign_fee_amount' should equal and 'total_cad' should equal + + Examples: + | amount | merchant_name | merchant_id | mcc | currency | exchange_rate | description | status | flag_field | flag_value | fee | total_cad | + | 1050.00 | GroceryMax | GROC5411 | 5411 | CAD | null | Essential boundary| 200 | over_limit_flag | true | null | null | + | 1050.01 | GroceryOver | GROC5411 | 5411 | CAD | null | Above boundary | 402 | error_code | INSUFFICIENT_FUNDS | null | null | + | 100.00 | USDHotel | HTL4722 | 4722 | USD | 1.250000 | FX fee compute | 200 | over_limit_flag | false | 3.75 | 128.75 | + + @api @AEGIS-TXN-003 + Scenario: Transaction frequency rate limiting with step-up MFA retry + Given I am authenticated with JWT cookies and have a valid X-CSRF-Token + When I submit 10 approved transactions of $1.00 within 60 minutes + And I submit an 11th $1.00 transaction + Then the response status should be 429 + And the error code should be 'FREQ_EXCEEDED' + And 'mfa_required' should be true + When I retry the 11th transaction with step-up MFA satisfied + Then the response status should be 200 + + # Owner-only Enforcement Across Endpoints + @api @AEGIS-AUTHZ-006 + Scenario Outline: Cross-account/card owner-only enforcement returns 403 with no leakage + Given User B is authenticated and owns different resources + When User B sends a request to '' + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + And the response body should not leak resource fields or PAN + + Examples: + | method | endpoint | + | GET | /accounts/{account_id_A}/summary | + | GET | /accounts/{account_id_A}/transactions?from_date=2026-04-01 | + | POST | /accounts/{account_id_A}/transactions | + | PATCH | /cards/{card_id_A}/status | + | PUT | /cards/{card_id_A}/pin | + | GET | /accounts/{account_id_A}/statements/{statement_id_A} | + | POST | /accounts/{account_id_A}/payments | + | POST | /notifications/webhook | + + @api @AEGIS-AUTHZ-006 + Scenario: Owner can access own summary while non-owner cannot + Given User A is authenticated and owns account_id_A + When User A sends a GET request to '/accounts/{account_id_A}/summary' + Then the response status should be 200 + And masked PAN should only appear if referenced + When User B sends a GET request to '/accounts/{account_id_A}/summary' + Then the response status should be 403 + And no data leakage should occur + + # Notifications Webhook Validation and Idempotency + @api @AEGIS-NOTIF-011 + Scenario Outline: Notifications webhook invalid inputs and authorization checks + Given I have a service authorization token '' + When I send a POST request to '/notifications/webhook' with JSON payload + """ + { + "account_id":"", + "alert_type":"", + "channel":"", + "severity":"", + "message_body":"", + "idempotency_key":"" + } + """ + Then the response status should be + And the error code should be '' + + Examples: + | service_auth | account_id | alert_type | channel | severity | message_body | idempotency_key | status | error_code | + | VALID | A | PAYMENT_REMINDER | IN_APP | INFO | Invalid alert | 11111111-2222-4333-8444-555555555555 | 400 | INVALID_ALERT_TYPE | + | VALID | A | STATEMENT_READY | FAX | INFO | Invalid channel | 11111111-2222-4333-8444-555555555556 | 400 | INVALID_ALERT_TYPE | + | VALID | A | STATEMENT_READY | IN_APP | INFO | Missing idempotency key | | 400 | VALIDATION_ERROR | + | MISSING | A | STATEMENT_READY | IN_APP | INFO | Unauthorized | 11111111-2222-4333-8444-555555555557 | 401 | FORBIDDEN | + + @api @AEGIS-NOTIF-011 + Scenario: Notifications webhook idempotency scoping per account and across channels + Given I have a valid service authorization token + When I send a POST request to '/notifications/webhook' with JSON payload + """ + { + "account_id":"A", + "alert_type":"STATEMENT_READY", + "channel":"IN_APP", + "severity":"INFO", + "message_body":"Your statement is ready", + "idempotency_key":"K1" + } + """ + Then the response status should be 200 + When I resend the same payload with idempotency_key 'K1' + Then the response status should be 409 + And the error code should be 'DUPLICATE_NOTIFICATION' + When I send the same logical alert to account 'B' with the same idempotency_key 'K1' + Then the response status should be 200 + And only account 'B' should receive the new delivery + + @api @AEGIS-NOTIF-LIMITS-030 + Scenario: Notifications webhook message length and channel-scoped idempotency per account + Given I have a valid service authorization token + When I send a POST request to '/notifications/webhook' with JSON payload exceeding 500 chars in message_body + Then the response status should be 400 + When I send a valid payload with exactly 500-char message_body and idempotency_key 'K-OVERLIM-2' + Then the response status should be 200 + And the in-app notification should render safely without scripts + When I resend the same payload with the same idempotency_key 'K-OVERLIM-2' but channel 'EMAIL' + Then the response status should be 409 + And the error code should be 'DUPLICATE_NOTIFICATION' + + # Email Verification and Login Enforcement + @api @ui @AEGIS-AUTH-VER-012 + Scenario: Email verification lifecycle with blocked pre-verification login and resend + Given I am on the registration page and have a CSRF token + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"Verity", + "last_name":"Quinn", + "email":"verify.qa+20260428@example.com", + "password":"Str0ng!Verify-Token", + "date_of_birth":"1990-04-28", + "phone_number":"+14165550199", + "ssn_last4":"1234", + "agree_terms":true + } + """ + Then the response status should be 201 + When I send a POST request to '/auth/login' with JSON payload + """ + { "email":"verify.qa+20260428@example.com","password":"Str0ng!Verify-Token" } + """ + Then the response status should be 403 + And the error code should be 'ACCOUNT_UNVERIFIED' + When I send a GET request to '/auth/verify-email?token={valid_verification_token}' + Then the response status should be 200 + And the response should indicate 'VERIFIED' + When I resend GET '/auth/verify-email?token={valid_verification_token}' + Then the response status should be 409 + And the error code should be 'ALREADY_VERIFIED' + When I send a POST request to '/auth/verification/resend' with JSON payload + """ + { "email":"verify.qa+20260428b@example.com" } + """ + Then the response status should be 200 + When I send a GET request to '/auth/verify-email?token=abc.def.ghi' + Then the response status should be 400 + And the error code should be 'INVALID_TOKEN' or 'TOKEN_EXPIRED' + When I send a POST request to '/auth/login' with valid MFA after verification + Then the response status should be 200 + And JWT cookies should be HttpOnly, Secure with SameSite=Strict + And no tokens should be stored in localStorage/sessionStorage + + # Refresh Rotation and Concurrency + @api @AEGIS-AUTH-REF-013 + Scenario: Refresh token rotation under multi-tab concurrency with CSRF continuity + Given Tab A is logged in with remember_me and has refresh_token 'R1' in cookies + And Tab B is opened with the same cookie jar + When Tab A sends POST '/auth/token/refresh' using 'R1' + Then the response status should be 200 and a new refresh_token 'R2' is set in cookies + When Tab B sends POST '/auth/token/refresh' using 'R1' + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When Tab B sends GET '/accounts/{account_id}/summary' with current cookies + Then the response status should be 200 + When Tab B sends POST '/auth/token/refresh' using 'R2' + Then the response status should be 200 and a new refresh_token 'R3' is set + When either tab retries refresh with 'R2' + Then the response status should be 401 and error 'TOKEN_INVALID' + When I send a POST request to '/accounts/{account_id}/transactions' without X-CSRF-Token + Then the response status should be 403 and error 'CSRF_MISSING' + When I retry the same POST with a valid X-CSRF-Token + Then the response status should be 200 and a 'transaction_id' is returned + + # Transactions History Filters and Pagination + @api @AEGIS-TXN-HIST-014 + Scenario Outline: Transactions history category filters, date-range validation, and paging + Given I am authenticated as the account owner + When I send a GET request to '/accounts/{account_id}/transactions' + Then the response status should be + And the results should match '' + + Examples: + | query | status | expectation | + | | 200 | Default cycle with mixed categories | + | ?category=PURCHASE | 200 | Only PURCHASE | + | ?category=REFUND | 200 | Only REFUND | + | ?from_date=2026-05-10&to_date=2026-05-01 | 400 | INVALID_DATE_RANGE | + | ?from_date=2026-04-01&to_date=2026-04-30 | 200 | Only in-range items | + | ?per_page=100&page=1 | 200 | Up to 100 items | + | ?per_page=25&page=9999 | 200 | Empty list with valid metadata | + | ?page=0 | 400 | Validation error for page min 1 | + + # Security and Session Management + @api @ui @AEGIS-SEC-004 + Scenario: Login lockout, per-IP rate limiting, refresh rotation, CSRF cross-site protection, inactivity timeout, and PAN masking + Given I am on the login page + When I attempt to login with wrong password 5 times within a short window + Then the first 4 responses should be 401 'INVALID_CREDENTIALS' and the 5th should be 403 'ACCOUNT_LOCKED' with 'unlock_at' + When I continue login attempts from the same IP exceeding 10/min + Then the response status should be 429 and error 'RATE_LIMITED' with 'retry_after' + When the unlock_at passes and I login with correct credentials and valid MFA + Then the response status should be 200 and cookies are HttpOnly, Secure, SameSite=Strict with no localStorage tokens + When I rotate the refresh token and immediately reuse the old refresh + Then the reuse response should be 401 'TOKEN_INVALID' and the session remains valid on the latest tokens + When I attempt a cross-site POST '/accounts/{account_id}/payments' without X-CSRF-Token + Then the response status should be 401 or 403 and no payment is created + When I include a valid X-CSRF-Token on a benign POST and submit + Then the response status should be 200 + When I idle for 13 minutes + Then a 2-minute warning modal should appear + When I reach 15 minutes of inactivity + Then I am auto-logged out and API calls return 401 requiring re-auth + And PAN is masked everywhere as '**** **** **** 1234' and never appears in DOM or logs + + # WebSocket Real-time Stream + @ws @AEGIS-WS-STREAM-015 + Scenario: Authorized subscription to own account, forbidden cross-account, schema validation, reconnect and dedupe + Given I logged in and have JWT cookies + When I connect to 'wss://realtime.aegiscard.com/v2/stream' with JWT + Then I should receive a 'connected' acknowledgment + When I subscribe to topic 'account:{account_id}' + Then I should receive a 'subscribed' confirmation + When a transaction event is published with message_id 'MSG-2001', type 'transaction.authorized', amount 42.50, mcc 5942, masked_pan '**** **** **** 1234' + Then I should see the event in the UI with required fields + When I attempt to subscribe to 'account:{other_account_id}' + Then I should receive 'SUBSCRIPTION_FORBIDDEN' and no data from that topic + When a malformed event is published + Then the client should ignore it without crashing + When the socket disconnects and reconnects with backoff + And the same event with message_id 'MSG-2001' and a new 'MSG-2002' are published + Then I should see exactly one instance of MSG-2001 and one of MSG-2002 + + @ws @AEGIS-WS-UNAUTH-029 + Scenario: WebSocket unauthorized handshake and subscribe/unsubscribe lifecycle + Given there is no JWT present + When I attempt to connect to 'wss://realtime.aegiscard.com/v2/stream' + Then the handshake should be rejected with 401 or 'auth_failed' + Given I login and have JWT cookies + When I connect and subscribe to 'account:{account_id}' + Then I receive a 'subscribed' ack + When I publish message_id 'MSG-1001' and verify one UI entry + And I unsubscribe from 'account:{account_id}' + And publish message_id 'MSG-1002' + Then no new entries should appear while unsubscribed + When I re-subscribe and publish 'MSG-1002' again and 'MSG-1003' + Then I should see one 'MSG-1002' and one 'MSG-1003' + And no tokens are stored in localStorage + + # Payments Scheduling and Types + @api @AEGIS-PAY-SCHED-016 + Scenario: Payment scheduling boundaries: min amount, past-date rejection, same-day immediate, and FULL_BALANCE + Given I am authenticated with JWT cookies and have a valid X-CSRF-Token + And I have retrieved '/accounts/{account_id}/summary' to capture total_balance and minimum_payment_due + When I send a POST request to '/accounts/{account_id}/payments' with JSON payload + """ + { "payment_amount":0.99,"payment_type":"CUSTOM","bank_account_id":"BANK123" } + """ + Then the response status should be 400 + And the error code should be 'BELOW_MINIMUM' + When I send a POST request to '/accounts/{account_id}/payments' with JSON payload + """ + { "payment_amount":5.00,"payment_type":"CUSTOM","bank_account_id":"BANK123","scheduled_date":"{yesterday}" } + """ + Then the response status should be 400 + And the error code should be 'SCHEDULED_DATE_INVALID' + When I send a POST request to '/accounts/{account_id}/payments' with JSON payload + """ + { "payment_amount":5.00,"payment_type":"CUSTOM","bank_account_id":"BANK123","scheduled_date":"{today}" } + """ + Then the response status should be 200 + And the response should treat it as immediate (no scheduled_date echoed) + When I send a POST request to '/accounts/{account_id}/payments' with JSON payload + """ + { "payment_type":"FULL_BALANCE","payment_amount":{total_balance},"bank_account_id":"BANK123" } + """ + Then the response status should be 200 + And 'new_balance_estimate' should be 0.00 + When I send a POST request to '/accounts/{account_id}/payments' without X-CSRF-Token + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + + @api @AEGIS-PAY-TYPES-025 + Scenario: MINIMUM and STATEMENT_BALANCE payments and precision enforcement + Given I captured 'minimum_payment_due' and 'statement_balance' from the current statement + When I send POST '/accounts/{account_id}/payments' with JSON + """ + { "payment_amount":10.999,"payment_type":"CUSTOM","bank_account_id":"BANK123" } + """ + Then the response status should be 400 + When I send POST '/accounts/{account_id}/payments' with JSON + """ + { "payment_amount":{total_balance_plus_one_cent},"payment_type":"CUSTOM","bank_account_id":"BANK123" } + """ + Then the response status should be 400 or 422 + When I send POST '/accounts/{account_id}/payments' with JSON + """ + { "payment_type":"MINIMUM","bank_account_id":"BANK123" } + """ + Then the response status should be 200 + And the response should include 'payment_id' and 'new_balance_estimate' + When I send POST '/accounts/{account_id}/payments' with JSON + """ + { "payment_type":"STATEMENT_BALANCE","bank_account_id":"BANK123" } + """ + Then the response status should be 200 + And 'new_balance_estimate' should reflect the statement balance reduction + When I resend the last POST without X-CSRF-Token + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + + # Card Controls: Lost/Stolen and Delivery Address Override + @api @AEGIS-CARD-LOST-009 + Scenario: Report lost/stolen irreversible flow, OTP failures, invalid transitions, and PIN format enforcement + Given I am authenticated with JWT cookies and have a valid X-CSRF-Token and an Active card + When I send a PATCH request to '/cards/{card_id}/status' with JSON + """ + { "status":"Frozen","reason":"Traveling","confirm_otp":"000000" } + """ + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I send a PUT request to '/cards/{card_id}/pin' with JSON + """ + { "new_pin":"12345","confirm_pin":"12345","session_otp":"123456" } + """ + Then the response status should be 400 + And the error code should be 'PIN_FORMAT' + When I send a PUT request to '/cards/{card_id}/pin' with JSON + """ + { "new_pin":"1234","confirm_pin":"4321","session_otp":"123456" } + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + When I send a POST request to '/cards/{card_id}/report-lost' with JSON + """ + { "loss_type":"STOLEN","last_known_use":"2026-04-28T10:00:00Z" } + """ + Then the response status should be 200 + And the response should include 'blocked_card_id','new_card_eta','case_number' + When I attempt to PATCH '/cards/{card_id}/status' back to 'Active' with valid OTP + Then the response status should be 400 + And the error code should be 'INVALID_TRANSITION' + When I send a PUT request to '/cards/{card_id}/pin' with JSON + """ + { "new_pin":"2580","confirm_pin":"2580","session_otp":"123456" } + """ + Then the response status should be 403 + And the error code should be 'CARD_BLOCKED' + When I resend POST '/cards/{card_id}/report-lost' + Then the response status should be 409 + And the error code should be 'ALREADY_BLOCKED' + When I send a POST request to '/accounts/{account_id}/transactions' after block + """ + { + "transaction_amount":5.00,"merchant_name":"Books","merchant_id":"BK1", + "mcc_code":"5942","transaction_type":"PURCHASE","description":"Test" + } + """ + Then the response status should be 403 + And the error code should be 'CARD_INACTIVE' + And all responses should mask PAN if referenced + + @api @AEGIS-CARD-DELIV-019 + Scenario: Report lost/stolen with delivery address override validation and replacement confirmation + Given I am authenticated with JWT cookies and have a valid X-CSRF-Token and an Active card + When I send a POST request to '/cards/{card_id}/report-lost' with JSON + """ + { + "loss_type":"LOST", + "last_known_use":"2026-04-28T10:00:00Z", + "delivery_address":{"street":"100 King St W","city":"Toronto","province":"Ontario","postal_code":"M5H 1J9"} + } + """ + Then the response status should be 400 + When I send a POST request to '/cards/{card_id}/report-lost' with JSON + """ + { + "loss_type":"LOST", + "last_known_use":"2026-04-28T10:00:00Z", + "delivery_address":{"street":"100 King St W","city":"Toronto","province":"ON","postal_code":"123 456"} + } + """ + Then the response status should be 400 + When I send a POST request to '/cards/{card_id}/report-lost' with JSON + """ + { + "loss_type":"STOLEN", + "last_known_use":"2026-04-28T10:00:00Z", + "delivery_address":{"street":"100 King St W","city":"Toronto","province":"ON","postal_code":"M5H 1J9"} + } + """ + Then the response status should be 200 + And the response should include 'blocked_card_id','new_card_eta','case_number' + And the UI should show Blocked banner and replacement to the override address + + # Refresh Token Expiry and Recovery + @api @AEGIS-AUTH-EXPIRE-020 + Scenario: Refresh token TTL expiry, 401 on refresh, re-authentication, and CSRF continuity + Given I login with remember_me false and have refresh_token 'R1' + And I fast-forward time beyond refresh TTL + When I send POST '/auth/token/refresh' using expired 'R1' + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I send GET '/accounts/{account_id}/summary' with expired access token + Then the response status should be 401 + When I re-login with valid MFA and obtain fresh tokens 'R2' + Then POST '/auth/token/refresh' with 'R2' returns 200 and rotates to 'R3' + And reusing 'R2' returns 401 'TOKEN_INVALID' + When I send POST '/accounts/{account_id}/payments' without X-CSRF-Token + Then the response status should be 403 'CSRF_MISSING' + When I resend the POST with a valid X-CSRF-Token + Then the response status should be 200 and includes 'payment_id' and 'new_balance_estimate' + + # Account Summary and Rewards Toggle + @api @AEGIS-SUMMARY-REW-021 + Scenario: Summary include_rewards toggle, rewards floor verification, and owner-only enforcement + Given I am authenticated and have a valid X-CSRF-Token + When I post a Travel PURCHASE of 88.88 CAD (MCC 4722) and a non-Travel PURCHASE of 19.99 CAD (MCC 5942) + Then both responses should be 200 + When I send GET '/accounts/{account_id}/summary' + Then the response status should be 200 and 'points_balance' should be absent + When I send GET '/accounts/{account_id}/summary?include_rewards=true' + Then the response status should be 200 + And 'points_balance' should reflect floor(88.88*3)+floor(19.99*1)=266+19=285 over baseline + When I send GET '/accounts/{other_account_id}/summary' + Then the response status should be 403 and no data leakage + + # Application Step 2 Idempotency and Session Misuse + @api @AEGIS-APP-IDEMP-022 + Scenario: Step 2 idempotency and cross-application session token misuse + Given I started application A1 and received session_token S1 + When I send POST '/applications/A1/financials' with X-App-Session S1 + Then the response status should be 200 with 'PENDING_REVIEW' and 'fico_pull_id' F1 + When I resend the same POST for A1 with S1 + Then the response status should be 200 and 'fico_pull_id' equals F1 (idempotent) + When I fire two concurrent POSTs for A1 with S1 + Then at most one soft pull should be created (same F1 reused) + When I start application A2 and receive session_token S2 + And I attempt POST '/applications/A2/financials' with X-App-Session S1 + Then the response status should be 401 and error 'SESSION_EXPIRED' + When I resend for A2 with X-App-Session S2 + Then the response status should be 200 with 'fico_pull_id' F2 different from F1 + When I complete Step 3 for A1 via POST '/applications/A1/submit' + Then no additional credit pull occurs + When I attempt Step 2 for A1 again with S1 post-decision + Then the response status should be 401 or 400 invalid state + + # CSRF Token Binding Across Session Changes + @api @AEGIS-CSRF-BIND-023 + Scenario: CSRF token binding and invalid-token rejection across endpoints + Given I login and load CSRF token T1 from bootstrap + When I send POST '/accounts/{account_id}/transactions' with X-CSRF-Token T1 + """ + { "transaction_amount":1.00,"merchant_name":"Books","merchant_id":"BK1","mcc_code":"5942","transaction_type":"PURCHASE","description":"Test" } + """ + Then the response status should be 200 + When I log out and log back in to obtain CSRF token T2 + Then T2 should differ from T1 + When I send POST '/accounts/{account_id}/payments' with X-CSRF-Token T1 + """ + { "payment_amount":5.00,"payment_type":"CUSTOM","bank_account_id":"BANK123" } + """ + Then the response status should be 403 and error 'CSRF_INVALID' + When I resend with X-CSRF-Token T2 + Then the response status should be 200 + When I send PATCH '/cards/{card_id}/status' with X-CSRF-Token T1 + """ + { "status":"Frozen","reason":"Pause","confirm_otp":"123456" } + """ + Then the response status should be 403 and error 'CSRF_INVALID' + When I resend PATCH with X-CSRF-Token T2 and valid OTP + Then the response status should be 200 and new_status 'Frozen' + When I send PUT '/cards/{card_id}/pin' with X-CSRF-Token T1 + """ + { "new_pin":"1234","confirm_pin":"1234","session_otp":"123456" } + """ + Then the response status should be 403 and error 'CSRF_INVALID' + When I resend PUT with X-CSRF-Token T2 + Then the response status should be 200 + When I send DELETE '/accounts/{id}' with X-CSRF-Token T1 + Then the response status should be 403 and error 'CSRF_INVALID' + + # Essential Buffer Lifecycle + @api @AEGIS-TXN-BUFFER-LIFE-024 + Scenario: Essential over-limit buffer lifecycle with recovery after payment + Given available_credit is exactly 1,000.00 CAD on '/accounts/{account_id}/summary' + When I POST an essential PURCHASE of 600.00 (MCC 5411) with CSRF + Then the response status should be 200 and over_limit_flag false + When I POST an essential PURCHASE of 450.00 (MCC 4900) with CSRF + Then the response status should be 200 and over_limit_flag true and available_credit near -50.00 + When I POST a non-essential PURCHASE of 5.00 (MCC 5942) + Then the response status should be 402 and error 'INSUFFICIENT_FUNDS' + When I POST an essential PURCHASE of 51.00 (MCC 5411) + Then the response status should be 402 and error 'INSUFFICIENT_FUNDS' + When I POST a payment of 60.00 CUSTOM with CSRF + Then the response status should be 200 + When I GET '/accounts/{account_id}/summary' + Then available_credit should be >= 10.00 + When I POST a non-essential PURCHASE of 5.00 (MCC 5942) + Then the response status should be 200 + When I POST any transaction without X-CSRF-Token + Then the response status should be 403 and error 'CSRF_MISSING' + And cross-account attempts should return 403 'FORBIDDEN' with no leakage + + # Transactions High-Value and FX Precision + @api @AEGIS-TXN-MAXFX-028 + Scenario: Transactions maximum amount and FX exchange_rate precision with REQ-006 rounding + Given I am authenticated with sufficient available_credit (>= 10,000,000.00) and have CSRF + When I POST a CAD PURCHASE with transaction_amount 10000000.00 (MCC 5942) + Then the response status should be 400 or 422 for amount exceeding Decimal(10,2) max + When I POST a CAD PURCHASE with transaction_amount 9999999.99 (MCC 5942) + Then the response status should be 200 and no foreign_fee_amount present + When I POST an EUR PURCHASE with amount 1234.56 and exchange_rate 1.2345678 (7 dp) + Then the response status should be 400 for exchange_rate precision violation + When I POST an EUR PURCHASE with amount 1234.56 and exchange_rate 1.234567 (6 dp) (MCC 4722) + Then the response status should be 200 + And foreign_fee_amount and total_cad are computed per REQ-006 and rounded to two decimals + When I POST an FX PURCHASE with exchange_rate 0.000000 + Then the response status should be 400 for invalid exchange_rate > 0 + When I POST a minimal positive CAD PURCHASE of 0.01 + Then the response status should be 200 if credit allows + + # Trusted Device and remember_me TTL + @api @AEGIS-AUTH-TRUST-017 + Scenario: Trusted device remember_me 30-day TTL and MFA suppression on known device + Given I attempt login from Device D1 with remember_me true and provide MFA + Then the response status should be 200 + And refresh_token cookie TTL should be approximately 30 days + When I login again from Device D1 with remember_me true and omit MFA + Then the response status should be 200 due to trusted device + When I login from Device D2 with remember_me false and omit MFA + Then MFA is required and upon submission the response is 200 + And refresh_token TTL is shorter than 30 days + When I rotate refresh on D1 and reuse the old refresh + Then the reuse returns 401 'TOKEN_INVALID' + When I POST a $1.00 PURCHASE with CSRF + Then the response status should be 200 + When I retry the same POST without CSRF + Then the response status should be 403 'CSRF_MISSING' + + # Right to Rescind Day-14 Boundary + @api @AEGIS-RESCIND-BOUND-018 + Scenario: Rescind on exact Day 14 with CSRF enforcement and post-closure behavior + Given the account issuance date is exactly 14 days ago and I have CSRF + When I send DELETE '/accounts/{id}' without X-CSRF-Token + Then the response status should be 403 'CSRF_MISSING' + When I resend DELETE '/accounts/{id}' with X-CSRF-Token + Then the response status should be 200 or 204 + When I GET '/accounts/{id}/summary' + Then account_status should be 'Closed' + When I POST '/accounts/{account_id}/transactions' with CSRF + Then the response status should be 403 'FORBIDDEN' or 'CARD_INACTIVE' + When I PATCH '/cards/{card_id}/status' with CSRF + Then the response status should be 400 'INVALID_TRANSITION' + When I POST '/accounts/{account_id}/payments' with CSRF + Then the response status should be 403 'FORBIDDEN' + And the audit trail should record the rescind event with required fields + + # Global PII Leakage Sweep + @api @ui @AEGIS-PII-SWEEP-026 + Scenario: Ensure no PII leakage on error payloads and UI across modules + Given I prepare invalid and cross-account requests + When I POST '/auth/register' with invalid email and underage DOB + Then the response status should be 400 with field errors and no PAN/SSN (only masked) + When I POST '/auth/login' with wrong password + Then the response status should be 401 'INVALID_CREDENTIALS' and no PII in body + When I POST '/accounts/{account_id}/transactions' with transaction_amount 0.00 + Then the response status should be 422 'INVALID_AMOUNT' and no PII + When I GET '/accounts/{other_account_id}/summary' + Then the response status should be 403 'FORBIDDEN' with no leakage + When I GET '/accounts/{account_id}/statements/{random_id}' + Then the response status should be 404 'NOT_FOUND' with no PII + When I POST '/accounts/{account_id}/payments' with invalid bank_account_id + Then the response status should be 422 'INVALID_BANK_ACCOUNT' and no PAN + When I PATCH '/cards/{card_id}/status' with bad OTP + Then the response status should be 401 'OTP_FAILED' and no PII + When I PUT '/cards/{card_id}/pin' with non-numeric PIN + Then the response status should be 400 'PIN_FORMAT' and no PII + When I POST '/applications/{application_id}/submit' without CSRF + Then the response status should be 403 'CSRF_MISSING' with no PII + And browser console/network logs should show no full PAN or SSN + + # Application Draft Auto-save and Resume (UI-centric with API submit) + @ui @api @AEGIS-APP-AUTOSAVE-027 + Scenario: Draft auto-save at 60s, sanitized localStorage, resume, submit clears draft, and inactivity warning + Given I am on Credit Application Step 1 and a CSRF token is loaded + And there are no tokens in localStorage/sessionStorage + When I fill in Step 1 fields without submitting + And I wait at least 60 seconds + Then a draft key (e.g., 'aegis.application.draft') should appear in localStorage + And the draft JSON should contain only Step 1 data with no SSN or PAN and a recent lastSaved timestamp + When I restart the browser and return to the application + Then I should be prompted to 'Resume draft' and fields should repopulate + When I stay idle for 13 minutes + Then a 2-minute warning modal appears + When I click 'Stay signed in' + Then the modal closes and form data remains intact + When I click Continue to submit Step 1 and the portal sends POST '/applications/start' with X-CSRF-Token + Then the response status should be 201 with 'application_id' and 'session_token' + And the draft key should be cleared or marked consumed + And reloading the page should not offer resume + diff --git a/functional_tests/functional-test-generation/functional-test-generation.json b/functional_tests/functional-test-generation/functional-test-generation.json new file mode 100644 index 0000000..f14bdd0 --- /dev/null +++ b/functional_tests/functional-test-generation/functional-test-generation.json @@ -0,0 +1,932 @@ +[ + { + "type": "end-to-end", + "title": "Approved end-to-end: Registration to rescind with card controls, FX transaction, payments, notifications", + "description": "Covers registration, login with MFA, token rotation, full credit application approval path, card freeze/unfreeze with OTP, virtual PIN set, CAD and FX purchase with fee, transaction rate limit, transaction list and summary, payments (immediate and scheduled), notifications idempotency, PAN masking, audit logging, and right to rescind within 14 days.", + "testId": "AEGIS-E2E-001", + "testDescription": "Validate the happy-path lifecycle for a new cardholder from account creation through approval and card usage, including security and regulatory controls.", + "prerequisites": "Clean email not in system (e.g., user+ts@example.com), test device for OTP/MFA, test bank account linked for payments, credit bureau stub configured to return FICO 720 for this applicant, test account currency CAD.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com/register and submit POST /v2/auth/register with valid fields: first_name 'Alice', last_name 'Reid', email 'alice.qa+20260428@example.com', password meeting >=12 chars and class rules, date_of_birth '1998-04-28', phone '+14165550123', ssn_last4 '1234', agree_terms true; verify client-side validations fire for empty fields before submit.\n2. Verify 201 response and UI shows 'Verification email sent'; ASSUMPTION: simulate email verification completion via internal tool; ensure no PII leakage and PAN not present anywhere.\n3. Open login, submit POST /v2/auth/login with email and password, valid 6-digit mfa_code, device_id UUID v4, remember_me true; expect 200 with JWT set as HttpOnly, Secure cookies; verify SPA stores no tokens in localStorage or sessionStorage.\n4. Call POST /v2/auth/token/refresh using the issued refresh_token; verify 200 with new access_token and rotated refresh_token; immediately retry with the old refresh token and expect 401 TOKEN_INVALID; confirm cookies updated.\n5. Start credit application: POST /v2/applications/start with full_legal_name 'Alice Marie Reid', email matching authenticated user, phone '+14165550123', residential_address valid (e.g., '100 King St W', 'Toronto', 'ON', 'M5H 1J9'), id_type 'DRIVERS_LICENSE', id_number 'R3ID12345'; expect 201 returning application_id and session_token; verify draft auto-save into localStorage every 60s and resume link in UI.\n6. Submit financials: POST /v2/applications/{application_id}/financials with header X-App-Session: session_token; employment_status EMPLOYED, employer_name 'Aegis QA Labs', gross_annual_income 95000.00, other_income omitted, monthly_rent 1800.00, existing_debt_payments 250.00, sin_consent true; expect 200 PENDING_REVIEW with fico_pull_id; ensure CSRF header present and SameSite=Strict cookies.\n7. Submit decision: POST /v2/applications/{application_id}/submit with card_product_id 'AEGIS_GOLD' and e_signature base64 of 'Alice M Reid'; with bureau returning FICO 720 expect 200 APPROVED with credit_limit and card_number_masked; verify masked format '**** **** **** 1234' only.\n8. Validate PAN masking across UI and API payloads (DOM inspection, network inspector); ensure SSN displayed as ***-**-1234 if shown; confirm audit trail NFR-04 recorded credit_limit change with user_id, session_id, ip_address, timestamp_utc.\n9. Freeze card: PATCH /v2/cards/{card_id}/status with status 'Frozen', reason 'Traveling', confirm_otp valid; expect 200 new_status Frozen; then Unfreeze with status 'Active' and valid OTP; verify INVALID_TRANSITION not returned and audit log contains entries.\n10. Set virtual PIN: PUT /v2/cards/{card_id}/pin with new_pin '2580', confirm_pin '2580', session_otp valid; expect 200 success and updated_at; ensure 4-digit format enforced and PIN not echoed in UI logs.\n11. Initiate CAD purchase: POST /v2/accounts/{account_id}/transactions with transaction_amount 150.00, merchant_name 'Maple Books', merchant_id 'MB123', mcc_code '5942', currency_code omitted (CAD default), transaction_type 'PURCHASE', description 'Books'; expect 200 with transaction_id, available_credit updated, auth_code present.\n12. Initiate FX purchase: POST /v2/accounts/{account_id}/transactions with transaction_amount 100.00, merchant_name 'Hotel Euro', merchant_id 'HTL4722', mcc_code '4722', currency_code 'USD', exchange_rate 1.250000, transaction_type 'PURCHASE', description 'Hotel'; expect 200 with itemized foreign_fee_amount 3.75 and Total_CAD computed as (100.00×1.250000)×1.03=128.75; verify precision to 2 decimals.\n13. Hit transaction frequency limit: submit 9 additional small purchases within 60 minutes, then a 11th; expect 429 FREQ_EXCEEDED with mfa_required true; retry 11th providing step-up MFA per UI and expect approval if funds available.\n14. List transactions: GET /v2/accounts/{account_id}/transactions with from_date, to_date spanning current cycle, page=1, per_page=25, category filter PURCHASE; expect 200 with pagination info and only purchases; try page beyond total_pages and expect empty list with valid metadata.\n15. View account summary: GET /v2/accounts/{account_id}/summary?include_rewards=true; expect current_balance, available_credit, credit_limit, account_status 'Active', billing_cycle_end, points_balance reflecting rewards floor rules; verify rewards from 4722 transaction apply floor(amount×3) on CAD-converted amount as per product rules.\n16. Make immediate payment: POST /v2/accounts/{account_id}/payments with payment_amount 200.00, payment_type 'CUSTOM', bank_account_id of pre-linked account; expect 200 accepted with new_balance_estimate; verify BELOW_MINIMUM not triggered.\n17. Schedule future payment: POST /v2/accounts/{account_id}/payments with payment_amount 100.00, payment_type 'CUSTOM', bank_account_id, scheduled_date 10 days in future; expect 200 with scheduled_date echoed.\n18. Notifications webhook STATEMENT_READY: POST /v2/notifications/webhook with account_id, alert_type 'STATEMENT_READY', channel 'IN_APP', severity 'INFO', message_body 'Your statement is ready', idempotency_key UUID; expect 200; re-post same idempotency_key and expect 409 DUPLICATE_NOTIFICATION; verify in-app toast appears.\n19. Exercise right to rescind within 14 days: DELETE /v2/accounts/{account_id}; ASSUMPTION: call available post-issuance and pre-activation window; expect 200 or 204 indicating account closed without fee; verify card access disabled and UI reflects closed status.\n20. Confirm immutable audit trail for rescind action exists and no full PAN present in any response or UI elements.", + "expectedResult": "User successfully registers, logs in with MFA, rotates tokens correctly, completes application and is APPROVED with masked PAN, can freeze/unfreeze card and set PIN via OTP, completes CAD and USD purchases with correct FX fee math, encounters transaction frequency limit requiring MFA, can list and summarize transactions with correct rewards and balances, makes immediate and scheduled payments, receives idempotent notifications, and rescinds account within 14 days; all state-changing calls include X-CSRF-Token, JWT cookies are HttpOnly/Secure with SameSite=Strict, and audit logs capture credit_limit change and rescind events.", + "apiEndpoint": "POST /v2/auth/register\nPOST /v2/auth/login\nPOST /v2/auth/token/refresh\nPOST /v2/applications/start\nPOST /v2/applications/{application_id}/financials\nPOST /v2/applications/{application_id}/submit\nPATCH /v2/cards/{card_id}/status\nPUT /v2/cards/{card_id}/pin\nPOST /v2/accounts/{account_id}/transactions\nGET /v2/accounts/{account_id}/transactions\nGET /v2/accounts/{account_id}/summary\nPOST /v2/accounts/{account_id}/payments\nPOST /v2/notifications/webhook\nDELETE /v2/accounts/{account_id}", + "httpMethod": "POST, POST, POST, POST, POST, POST, PATCH, PUT, POST, GET, GET, POST, POST, DELETE", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json\nX-App-Session: (for applications financials)\nCookie: HttpOnly Secure JWT cookies", + "requestBody": "Register: first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms\nLogin: email,password,mfa_code,device_id,remember_me\nFinancials: employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent\nSubmit: card_product_id,e_signature\nCard status: status,reason,confirm_otp\nPIN: new_pin,confirm_pin,session_otp\nTransactions: transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description\nPayments: payment_amount,payment_type,bank_account_id,scheduled_date\nWebhook: account_id,alert_type,channel,message_body,severity,idempotency_key", + "responseCodesExpected": "201,200,400,401,403,409,422,429", + "role": "Cardholder", + "stateBefore": "No user account, no session, no application, no card or account", + "stateAfter": "Account rescinded/closed; card controls inoperative; audit logs present; no PAN exposed", + "calculationFormula": "FX Fee (REQ-006): Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03", + "mccCode": "5942 Books, 4722 Travel", + "currency": "CAD, USD", + "csrfTokenPresent": "true for valid state-changing requests", + "authTokenType": "OAuth 2.0 PKCE, JWT in HttpOnly, Secure cookies, SameSite=Strict", + "piiMaskingExpected": "PAN **** **** **** 1234 only, SSN ***-**-1234, no raw PAN in DOM or API", + "auditLogExpected": "credit_limit change recorded with user_id,session_id,ip_address,timestamp_utc; rescind action recorded immutably", + "regulatoryRefs": "PCI-DSS L1,NFR-05 CSRF,NFR-06 Session Timeout,REQ-006,REQ-014,REQ-016,NFR-04", + "errorCodeExpected": "TOKEN_INVALID, DUPLICATE_NOTIFICATION", + "paginationParams": "page=1..n, per_page up to 100", + "featureFlag": "", + "assumptions": "ASSUMPTION: Email verification auto-completed in test harness; ASSUMPTION: Rescind endpoint returns 200/204; ASSUMPTION: Rewards on FX use CAD-converted amount for points; ASSUMPTION: All times UTC." + }, + { + "type": "end-to-end", + "title": "Application decision boundaries and session token expiry with save/resume", + "description": "Validates application session expiry handling and FICO boundary decisions for APPROVED, PENDING, DECLINED. Includes negative signature error and UI resume from auto-saved draft.", + "testId": "AEGIS-E2E-002", + "testDescription": "Ensure X-App-Session expiry errors are handled, drafts can be resumed, and FICO thresholds at 599/600/680/681 lead to correct decisions, with correct messaging and codes.", + "prerequisites": "Multiple test emails not in system (e.g., bob.qa+ts@example.com, cara.qa+ts@example.com, dan.qa+ts@example.com), credit bureau stubs configured to return FICO 599, 600, 680, 681 for respective identities.", + "stepsToPerform": "1. Register and login as 'Bob' (bob.qa+20260428@example.com) with MFA; verify JWT cookies set and CSRF token loaded.\n2. Start application Step 1 for Bob with valid personal and ID data; capture application_id and session_token; confirm localStorage draft exists and UI shows 'Resume later'.\n3. Intentionally wait >30 minutes (ASSUMPTION: simulate via clock skew or config) to expire X-App-Session; attempt Step 2 POST /v2/applications/{application_id}/financials with expired token; expect 401 error SESSION_EXPIRED and UI prompt to resume.\n4. Use UI 'Resume Application' to reload draft (from localStorage) and re-initiate Step 1 to obtain a fresh application_id and session_token (ASSUMPTION: re-creating application allowed if prior session expired); verify 201 and new identifiers returned.\n5. Submit Step 2 successfully with sin_consent true and valid financials; expect 200 PENDING_REVIEW with fico_pull_id.\n6. Submit Step 3 without e_signature to trigger negative case; expect 400 SIGNATURE_REQUIRED with appropriate field error; ensure no decision created and UI highlights the signature field.\n7. Resubmit Step 3 with valid e_signature; for FICO 681 (Bob) expect 200 with decision APPROVED and masked card number; verify FICO > 680 rule.\n8. Repeat Steps 1–7 for 'Cara' configured at FICO 680; on Step 3 expect 200 decision PENDING with review_eta_hours present; ensure inclusivity at 680 leads to PENDING not APPROVED.\n9. Repeat Steps 1–7 for 'Dan' configured at FICO 600; on Step 3 expect 200 PENDING with review_eta_hours; verify inclusivity at 600.\n10. Repeat Steps 1–7 for 'Eve' configured at FICO 599; on Step 3 expect 200 DECLINED with reason_code provided; ensure UI shows appropriate decline messaging and no card details.\n11. Confirm email/portal notifications differ by decision: APPROVED shows masked PAN, PENDING shows ETA only, DECLINED shows reason only; verify no unmasked PII and audit logs exist only for approval credit_limit assignment.", + "expectedResult": "Expired X-App-Session yields 401 SESSION_EXPIRED; users can resume via draft and complete steps; FICO 681 returns APPROVED, FICO 680 and 600 return PENDING with ETA, FICO 599 returns DECLINED with reason; missing e_signature triggers 400 SIGNATURE_REQUIRED; masked PAN displayed only on approved path; audit trail for credit_limit only on approvals.", + "apiEndpoint": "POST /v2/auth/register\nPOST /v2/auth/login\nPOST /v2/applications/start\nPOST /v2/applications/{application_id}/financials\nPOST /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST, POST, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nX-App-Session: ", + "requestBody": "Register/Login: standard fields\nStart: full_legal_name,email,phone,residential_address,id_type,id_number\nFinancials: employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent\nSubmit: card_product_id,e_signature", + "responseCodesExpected": "201,200,400,401", + "role": "Cardholder", + "stateBefore": "No applications exist for test users", + "stateAfter": "One approved application (Bob), two pending (Cara, Dan), one declined (Eve); drafts consumed; no PAN exposed except masked on approved", + "calculationFormula": "Decision Table: APPROVED if FICO > 680; PENDING if 600–680 inclusive; DECLINED if < 600", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for all valid posts", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE", + "piiMaskingExpected": "Approved decision shows **** **** **** 1234 only; no PAN elsewhere", + "auditLogExpected": "Only approvals write credit_limit audit trail with user_id,session_id,ip_address,timestamp_utc", + "regulatoryRefs": "REQ-002 Decisioning,PCI-DSS L1,NFR-05 CSRF,NFR-04 Audit", + "errorCodeExpected": "SESSION_EXPIRED,SIGNATURE_REQUIRED", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Resume flow re-creates Step 1 if prior session expired; ASSUMPTION: Time simulated in UTC; ASSUMPTION: Different test identities map to configured FICO scores." + }, + { + "type": "functional", + "title": "Transactions: CSRF enforcement, essential over-limit buffer, FX fee precision, rate limiting, owner-only access", + "description": "Focuses on transaction validation rules including CSRF requirement, essential services 5% over-limit buffer, FX fee calculation and rounding, transaction frequency limits with MFA, and enforcement of owner-only access with pagination bounds.", + "testId": "AEGIS-TXN-003", + "testDescription": "Validate transaction API behaviors for security and business rules under normal and edge conditions.", + "prerequisites": "Existing active account_id and card in Active status with available_credit CAD 1,000.00; linked user logged in with valid JWT and CSRF token; test merchant MCCs configured; ASSUMPTION: essential MCCs include 5411 (Grocery) and 4900 (Utilities).", + "stepsToPerform": "1. Attempt to POST /v2/accounts/{account_id}/transactions without X-CSRF-Token while cookies present; expect rejection per NFR-05; ASSUMPTION: 403 with error 'CSRF_MISSING'.\n2. Submit valid CAD purchase with non-essential MCC to exceed available_credit (e.g., amount 1051.00 with MCC 5942); expect 402 INSUFFICIENT_FUNDS and available_credit echoed; ensure no change to balance.\n3. Submit essential service purchase within 5% buffer: amount 1049.00 (≤ 1,000 + 5%) with MCC 5411 Grocery; expect 200 with over_limit_flag true and authorization approved; verify available_credit reflects buffer usage.\n4. Submit transaction with transaction_amount 0.00; expect 422 INVALID_AMOUNT; then submit -10.00 and expect same; verify server enforces > 0.00.\n5. Submit FX transaction: 100.00 USD at exchange_rate 1.250000, MCC 4722 Travel; expect Total_CAD 128.75 and foreign_fee_amount 3.75; confirm both rounded to two decimals; verify math: (100×1.25)×1.03.\n6. Perform 10 approved micro-transactions within 60 minutes (e.g., $1.00 each) then attempt an 11th; expect 429 FREQ_EXCEEDED and mfa_required true; supply step-up MFA via UI and retry the 11th with MFA to obtain approval if funds available.\n7. Verify owner-only enforcement: as a different logged-in user, call GET /v2/accounts/{account_id}/transactions for the first user's account; expect 403 FORBIDDEN; confirm no data leakage.\n8. Test pagination boundaries: GET /v2/accounts/{account_id}/transactions with per_page=100 and page=1 returns up to 100 items; request per_page=101 (ASSUMPTION: server caps to 100) and verify response per_page reported as 100; request page=0 and expect server coerces to 1 or returns validation error (ASSUMPTION: coerced to 1).\n9. Confirm data masking: any transaction response that includes card references shows masked PAN only; ensure no full PAN in payloads.\n10. Validate card state check: temporarily Freeze card and retry a transaction; expect 403 CARD_INACTIVE with card_status 'Frozen'; unfreeze to restore normal behavior.", + "expectedResult": "CSRF-less POSTs are rejected; non-essential over-limit declines with 402; essential service over-limit within 5% approves with flag; invalid amounts rejected with 422; FX fee computed precisely; >10 txns/60min triggers 429 with MFA requirement; cross-account access returns 403; pagination respects limits; transactions reflect PAN masking; frozen card cannot transact until unfrozen.", + "apiEndpoint": "POST /v2/accounts/{account_id}/transactions\nGET /v2/accounts/{account_id}/transactions\nPATCH /v2/cards/{card_id}/status", + "httpMethod": "POST, GET, PATCH", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json", + "requestBody": "Transactions: transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description\nCard status: status,confirm_otp", + "responseCodesExpected": "200,402,403,422,429", + "role": "Cardholder", + "stateBefore": "Account Active with available_credit 1,000.00; card Active", + "stateAfter": "Some transactions posted including over-limit essential; card restored to Active; no security breaches", + "calculationFormula": "Over-limit buffer: approve essential up to available_credit × 1.05; FX: Total_CAD = (amount × rate) × 1.03; foreign_fee_amount = (amount × rate) × 0.03", + "mccCode": "5411 Grocery (essential), 5942 Books (non-essential), 4722 Travel", + "currency": "CAD, USD", + "csrfTokenPresent": "Mixed: false for negative test, true for valid requests", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only", + "auditLogExpected": "Card freeze/unfreeze actions logged with user_id,session_id,ip_address,timestamp_utc", + "regulatoryRefs": "NFR-05 CSRF,REQ-006 FX Fee,Owner-only enforcement", + "errorCodeExpected": "CSRF_MISSING, INSUFFICIENT_FUNDS, INVALID_AMOUNT, CARD_INACTIVE, FREQ_EXCEEDED", + "paginationParams": "page >= 1, per_page <= 100; server caps excess per_page", + "featureFlag": "", + "assumptions": "ASSUMPTION: Essential MCCs include 5411 and 4900; ASSUMPTION: CSRF failure returns 403 CSRF_MISSING; ASSUMPTION: Pagination out-of-range coerces to defaults." + }, + { + "type": "functional", + "title": "Security and session management: lockout, rate limiting, refresh rotation, SameSite CSRF, session timeout, WebSocket auth, PAN masking", + "description": "Validates authentication security controls: login failure lockout, per-IP rate limiting, refresh token single-use, CSRF with SameSite, session inactivity timeout with warning, WebSocket auth and reconnection, and no token storage in localStorage; also verifies PAN masking.", + "testId": "AEGIS-SEC-004", + "testDescription": "Ensure the system enforces strong auth/session controls and protects against CSRF and token leakage while supporting live stream auth.", + "prerequisites": "Existing user account with MFA enabled; test IP control to simulate >10 req/min; browser devtools access; test transaction publisher to realtime stream.", + "stepsToPerform": "1. Attempt to login with wrong password 5 times within a short time window via POST /v2/auth/login; expect 401 INVALID_CREDENTIALS for first 4 and then 403 ACCOUNT_LOCKED with unlock_at on 5th; ensure messaging on UI reflects lockout.\n2. Continue sending login attempts from same IP to surpass 10 requests/min; expect 429 RATE_LIMITED with retry_after; verify WAF/gateway rate limit header behavior.\n3. After unlock_at passes (ASSUMPTION: fast-forward in test), login successfully with correct credentials and valid mfa_code; verify HttpOnly, Secure JWT cookies set; confirm no tokens in localStorage/sessionStorage; confirm SameSite=Strict on cookies.\n4. Acquire refresh_token and call POST /v2/auth/token/refresh to rotate; verify new refresh_token differs; immediately reuse the previous refresh_token and expect 401 TOKEN_INVALID; confirm session remains valid with the latest tokens only.\n5. Attempt a state-changing POST (e.g., /v2/accounts/{account_id}/payments) from a cross-site context without X-CSRF-Token and with SameSite=Strict cookies not sent (simulate via different domain); expect request rejected due to missing auth/CSRF (401/403) and no payment created.\n6. Validate CSRF success path: fetch CSRF token via portal bootstrap, include X-CSRF-Token on a benign POST (e.g., initiate a $1 test transaction) and observe 200 success; ensure anti-CSRF cookie presence as appropriate.\n7. Test 15-minute inactivity timeout: login and navigate to Dashboard; remain idle for 13 minutes; verify 2-minute warning modal appears; at 15 minutes, verify auto-logout and redirect to login; attempt an API call and expect 401 requiring re-auth; fill a form prior to timeout and verify unsaved work handling via auto-save (ASSUMPTION: localStorage preserves drafts).\n8. Connect to WebSocket wss://realtime.aegiscard.com/v2/stream with JWT auth (ASSUMPTION: via Authorization header or subprotocol); subscribe to the account topic; publish a test transaction event; verify real-time UI feed updates; force token expiry to observe server closing the socket; refresh token and reconnect with exponential backoff; verify no duplicate messages on resume.\n9. Check PAN masking policy: browse card details and statements; inspect DOM and network responses to ensure PAN is masked as **** **** **** 1234 and full PAN is never transmitted; verify PCI-DSS ban on raw PAN in DOM.\n10. Ensure logs do not contain PII: check console logs and server-stub logs for absence of PAN or SSN; only masked values should appear.", + "expectedResult": "Account lockout occurs after 5 failed attempts with unlock_at shown; 429 returned when rate limits exceeded; refresh tokens rotate and cannot be reused; CSRF with SameSite prevents cross-site posts without token; session times out after 15 minutes with 2-minute warning and unsaved work preserved via drafts; WebSocket authenticates with JWT, handles token expiry and reconnects cleanly; PAN is always masked in UI and payloads; no PII in browser logs.", + "apiEndpoint": "POST /v2/auth/login\nPOST /v2/auth/token/refresh\nPOST /v2/accounts/{account_id}/payments\nwss://realtime.aegiscard.com/v2/stream", + "httpMethod": "POST, POST, POST, WS", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: for valid posts\nCookie: HttpOnly Secure, SameSite=Strict", + "requestBody": "Login: email,password,mfa_code\nPayments: payment_amount,payment_type,bank_account_id", + "responseCodesExpected": "200,401,403,429", + "role": "Cardholder", + "stateBefore": "User account exists and is unlocked; no active WebSocket connection", + "stateAfter": "User session validated then expired by inactivity; WebSocket reconnected; no CSRF vulnerabilities; no PAN leaks", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "Mixed: absent for negative cross-site test, present for valid requests", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE; refresh rotation enforced", + "piiMaskingExpected": "PAN **** **** **** 1234 only, no raw PAN in DOM", + "auditLogExpected": "No audit trail changes except standard auth/session logs; security events captured by WAF", + "regulatoryRefs": "PCI-DSS L1,NFR-05 CSRF,NFR-06 Session Timeout", + "errorCodeExpected": "INVALID_CREDENTIALS, ACCOUNT_LOCKED, RATE_LIMITED, TOKEN_INVALID", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Cross-site POST does not include SameSite=Strict cookies; ASSUMPTION: WebSocket auth via Authorization header or subprotocol; ASSUMPTION: Warning modal at 13-minute mark." + }, + { + "type": "functional", + "title": "Billing, rewards, statements, payments, webhook idempotency, rescind after day-14 rejection", + "description": "Verifies statement generation, ADB interest math (including leap year), grace period logic, late fee boundaries, rewards accrual with floor rounding, payment validations, notification webhooks for multiple alert types with idempotency, and right-to-rescind window after day 14.", + "testId": "AEGIS-BILL-005", + "testDescription": "Confirm financial calculations and billing rules REQ-009–REQ-015, payment rules, webhook idempotency, and rescind policy REQ-016.", + "prerequisites": "Account with prior statement history; test data for APR (e.g., 19.99%), transactions in Travel (4722) and non-Travel MCCs, known prev_statement_balance_paid_in_full values, payment due dates around DST/leap year; linked bank account and an invalid bank_account_id for negative case.", + "stepsToPerform": "1. Retrieve a valid statement via GET /v2/accounts/{account_id}/statements/{statement_id}?format=JSON; verify 200 with fields statement_date,total_spend,adb,interest_charged,late_fee,rewards_earned,minimum_payment_due,due_date; request a non-existent statement_id and expect 404 NOT_FOUND.\n2. Validate statement accuracy REQ-015 by summing transaction_amount[] from detailed view (ASSUMPTION: available via statements list or detail) and confirming within ±$0.01 equals total_spend; assert failure if outside tolerance.\n3. Compute interest REQ-009: using ADB and APR 19.99%, for a 30-day cycle interest = (ADB × 0.1999 / 365) × 30; verify interest_charged matches rounded output; repeat for a leap year February (29 days) to ensure divisor 365 still applies per spec and Days_in_Billing_Cycle adjusted.\n4. Grace period REQ-010: for cycle where prev_statement_balance_paid_in_full = true and purchases only, verify interest_charged = 0; for cycle where it is false, verify interest applies; confirm 21-day grace period rule.\n5. Late fee REQ-011 boundary: set payment_received_date equal to due_date + 2 days (23:59:59) and verify no late_fee; then test payment_received_date at due_date + 2 days + 1 minute and verify late_fee = $35.00; ensure timezone UTC handling across DST boundary is correct (ASSUMPTION: computations in UTC).\n6. Rewards accrual REQ-012 and REQ-013: include a Travel MCC 4722 purchase CAD 123.45 and a non-Travel purchase CAD 98.76; verify points = floor(123.45 × 3) + floor(98.76 × 1) = floor(370.35) + floor(98.76) = 370 + 98 = 468; confirm no rounding up occurs.\n7. Payments: POST /v2/accounts/{account_id}/payments with payment_amount less than minimum_payment_due and payment_type MINIMUM to trigger 400 BELOW_MINIMUM; then submit with invalid bank_account_id to trigger 422 INVALID_BANK_ACCOUNT; finally, submit valid immediate payment and a scheduled future payment and verify 200 responses with new_balance_estimate and scheduled_date.\n8. Notifications webhook: POST /v2/notifications/webhook for alert_type 'LATE_PAYMENT' (EMAIL, CRITICAL), then 'PIN_LOCKED' (PUSH, WARNING), 'FRAUD_FLAG' (SMS, CRITICAL), 'OVER_LIMIT' (IN_APP, WARNING), 'STATEMENT_READY' (IN_APP, INFO); expect 200 for valid first sends; immediately replay each with same idempotency_key to receive 409 DUPLICATE_NOTIFICATION; verify in-app and email/PUSH stubs receive one delivery each.\n9. Try to rescind after Day 14: for an account issued 15 days ago, call DELETE /v2/accounts/{id}; expect rejection (ASSUMPTION: 403 with error 'WINDOW_ELAPSED'); ensure account remains active and no fees waived; verify appropriate user messaging.\n10. Verify PAN masking on statements (PDF and JSON) and payments responses; ensure no full PAN is present in payloads or PDF content; ensure PCI-DSS compliance notes satisfied.", + "expectedResult": "Statements return accurate totals within tolerance; ADB interest matches REQ-009 across normal and leap-year cycles; grace period applies only when eligible; $35 late fee triggers strictly after due_date + 2 days; rewards apply floor rounding with Travel multiplier; payment validations enforce minimums and bank account linkage; webhooks are idempotent and accept valid alert types/channels; rescind after Day 14 is rejected; PAN is masked everywhere.", + "apiEndpoint": "GET /v2/accounts/{account_id}/statements/{statement_id}\nPOST /v2/accounts/{account_id}/payments\nPOST /v2/notifications/webhook\nDELETE /v2/accounts/{id}", + "httpMethod": "GET, POST, POST, DELETE", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json", + "requestBody": "Payments: payment_amount,payment_type,bank_account_id,scheduled_date\nWebhook: account_id,alert_type,channel,message_body,severity,idempotency_key", + "responseCodesExpected": "200,400,404,403,422", + "role": "Cardholder", + "stateBefore": "Account active with statements and transactions; payment due set; issuance dates known", + "stateAfter": "Payments accepted or rejected per rules; notifications delivered once; rescind after window rejected; no calculation discrepancies", + "calculationFormula": "Interest: (ADB × APR / 365) × Days_in_Billing_Cycle; Rewards: Travel floor(amount×3), Others floor(amount×1)", + "mccCode": "4722 Travel, various non-Travel", + "currency": "CAD", + "csrfTokenPresent": "true for all state-changing requests", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only in JSON/PDF; no full PAN", + "auditLogExpected": "No credit_limit audit changes unless product changes; standard payment and webhook audit events recorded", + "regulatoryRefs": "REQ-009,REQ-010,REQ-011,REQ-012,REQ-013,REQ-015,REQ-014,REQ-016,NFR-05", + "errorCodeExpected": "BELOW_MINIMUM, INVALID_BANK_ACCOUNT, DUPLICATE_NOTIFICATION, WINDOW_ELAPSED", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: All times UTC; ASSUMPTION: Rescind rejection returns 403 WINDOW_ELAPSED; ASSUMPTION: Statement detail accessible for sum check." + }, + { + "type": "functional", + "title": "Ownership and authorization: account/card owner-only enforcement across endpoints", + "description": "Ensures 403 FORBIDDEN is returned when accessing or mutating resources not owned by the authenticated user across transactions, summaries, card controls, PIN set, statements, and payments.", + "testId": "AEGIS-AUTHZ-006", + "testDescription": "Validate owner-only access control for account_id and card_id resources and confirm no data leakage in error responses.", + "prerequisites": "Two users: User A owns account_id_A and card_id_A; User B owns account_id_B and card_id_B; both accounts active; both users able to login.", + "stepsToPerform": "1. Login as User B and attempt GET /v2/accounts/{account_id_A}/summary; expect 403 FORBIDDEN; verify no account fields leaked in response body.\n2. Attempt GET /v2/accounts/{account_id_A}/transactions as User B with valid query params; expect 403 FORBIDDEN; confirm no transaction metadata (counts, totals) leaks.\n3. Attempt POST /v2/accounts/{account_id_A}/transactions as User B to create a purchase; include CSRF token; expect 403 FORBIDDEN due to owner-only enforcement; confirm no partial processing.\n4. Attempt PATCH /v2/cards/{card_id_A}/status (freeze) as User B with a valid OTP; expect 403 FORBIDDEN regardless of OTP validity; ensure attempts_remaining not decremented for User A's OTP.\n5. Attempt PUT /v2/cards/{card_id_A}/pin as User B with any session_otp; expect 403 CARD_BLOCKED or FORBIDDEN depending on policy; ASSUMPTION: 403 FORBIDDEN owner-only; verify no PIN policy responses reveal card state.\n6. Attempt GET /v2/accounts/{account_id_A}/statements/{statement_id_A} as User B; expect 403 FORBIDDEN (ASSUMPTION: 403 at gateway); ensure statement metadata not leaked.\n7. Attempt POST /v2/accounts/{account_id_A}/payments as User B with any bank_account_id; expect 403 FORBIDDEN; verify no funds movement queued and error contains no sensitive details.\n8. Attempt POST /v2/notifications/webhook with account_id_A as User B (if permitted only to internal engine); expect 403/401 depending on internal-only auth (ASSUMPTION: 403 FORBIDDEN in test harness); ensure idempotency_key and payload not processed.\n9. Switch to User A, repeat one of each endpoint correctly to confirm 200 success and normal operation; validate that only User A can view masked PAN details and perform state changes.\n10. Review logs to ensure no PII leakage in error messages for forbidden attempts; confirm consistent error structure and codes.", + "expectedResult": "All cross-account/card access attempts by non-owners result in 403 FORBIDDEN with no data leakage; owner can perform the same actions successfully; masked PAN appears only for resource owner; OTP and PIN endpoints do not reveal target card state to non-owner.", + "apiEndpoint": "GET /v2/accounts/{account_id}/summary\nGET /v2/accounts/{account_id}/transactions\nPOST /v2/accounts/{account_id}/transactions\nPATCH /v2/cards/{card_id}/status\nPUT /v2/cards/{card_id}/pin\nGET /v2/accounts/{account_id}/statements/{statement_id}\nPOST /v2/accounts/{account_id}/payments\nPOST /v2/notifications/webhook", + "httpMethod": "GET, GET, POST, PATCH, PUT, GET, POST, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: ", + "requestBody": "Transactions/Payments standard bodies; PIN and status bodies as per spec; webhook payload as per spec", + "responseCodesExpected": "403,200", + "role": "Cardholder", + "stateBefore": "Two separate users with their own accounts/cards", + "stateAfter": "No state changes for User A resources by User B; successful operations by User A only", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for attempted state-changing calls", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "No PAN in 403 responses; owner sees masked PAN only", + "auditLogExpected": "Forbidden attempts logged for security monitoring; no audit trail changes on resources", + "regulatoryRefs": "Owner-only enforcement,NFR-05 CSRF,PCI-DSS L1", + "errorCodeExpected": "FORBIDDEN", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Webhook endpoint is internal-only and rejects user calls; ASSUMPTION: PIN endpoint returns 403 FORBIDDEN for non-owners without revealing card state." + }, + { + "type": "functional", + "title": "Registration validation boundaries and duplicate handling", + "description": "Validate POST /v2/auth/register enforces all field rules: RFC 5322 email, unique email handling, password complexity with 422 WEAK_PASSWORD, DOB age >= 18 boundary, E.164 phone, SSN last4 digits, and agree_terms true. Ensure correct response codes and UI error messaging.", + "testId": "AEGIS-AUTH-REG-007", + "testDescription": "Negative, boundary, and positive validation coverage for user registration including duplicate email and weak password handling.", + "prerequisites": "Portal reachable at https://portal.aegiscard.com, API at https://api.aegiscard.com/v2; test clock in UTC; an existing account with email dup.qa@example.com already registered; browser obtains CSRF token on page load.", + "stepsToPerform": "1. Navigate to Registration page and load CSRF token; verify no tokens stored in localStorage per policy.\n2. Submit POST /v2/auth/register with underage DOB (17 years 364 days old), valid other fields, agree_terms true; expect field-level error for date_of_birth age < 18.\n3. Resubmit with invalid phone_number '4165551234' (missing + and country code) and valid others; expect 400 with field 'phone_number' E.164 violation message.\n4. Resubmit with ssn_last4 '12A4' (non-numeric) and then '123' (3 digits); expect 400 each time with specific field error for ssn_last4.\n5. Resubmit with weak password 'Short1!' (fails length/classes); expect 422 with error WEAK_PASSWORD and message referencing minimum 12 chars and required classes.\n6. Resubmit with agree_terms false and otherwise valid data; expect 400 with field 'agree_terms' must be true.\n7. Resubmit with invalid email 'user@@example..com' and then valid email new.qa+20260428@example.com but with first_name 'A1' (non-alpha) then 'A' (too short); expect 400 for each offending field until all corrected.\n8. Submit a fully valid payload: first_name 'Alex', last_name 'Rivera', email 'new.qa+20260428@example.com', password meeting >=12 and class rules, date_of_birth exactly 18 years ago (boundary), phone '+14165551234', ssn_last4 '1234', agree_terms true; expect 201 with user_id and verification notice in UI.\n9. Immediately retry registration with the same email 'new.qa+20260428@example.com'; expect 409 with error EMAIL_EXISTS.\n10. Verify UI/Network responses contain no PAN or SSN beyond masked (SSN ***-**-1234 if displayed), cookies are HttpOnly/Secure with SameSite=Strict, and no PII leaks in console/network logs.", + "expectedResult": "Server rejects malformed or out-of-policy fields with 400 field errors; weak passwords return 422 WEAK_PASSWORD; valid registration returns 201 and verification initiation; duplicate email returns 409 EMAIL_EXISTS; age exactly 18 is accepted; all responses and UI show no unmasked PII, tokens are not stored in localStorage.", + "apiEndpoint": "POST /v2/auth/register", + "httpMethod": "POST", + "requestHeaders": "Content-Type: application/json, X-CSRF-Token: ", + "requestBody": "first_name, last_name, email, password, date_of_birth (YYYY-MM-DD), phone_number (+E.164), ssn_last4 (4 digits), agree_terms (true)", + "responseCodesExpected": "201,400,409,422", + "role": "Prospective Applicant", + "stateBefore": "Email dup.qa@example.com already exists; target new.qa+20260428@example.com not registered", + "stateAfter": "Account created for new.qa+20260428@example.com; no creation on invalid attempts; duplicate attempt rejected", + "calculationFormula": "", + "mccCode": "", + "currency": "", + "csrfTokenPresent": "true", + "authTokenType": "OAuth 2.0 with PKCE, JWT cookies on later login only", + "piiMaskingExpected": "SSN ***-**-1234 if echoed, no PAN present anywhere", + "auditLogExpected": "Standard auth audit only; no credit_limit changes", + "regulatoryRefs": "NFR-05 CSRF, PCI-DSS L1", + "errorCodeExpected": "EMAIL_EXISTS, WEAK_PASSWORD", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Registration respects NFR-05 CSRF even for public endpoint; ASSUMPTION: All times UTC for DOB computation" + }, + { + "type": "functional", + "title": "Credit application Step 1 and Step 2 validations with CSRF enforcement and duplicate application prevention", + "description": "Validate field-level and conditional validations for application Step 1 and Step 2, enforce CSRF and X-App-Session headers, and ensure 409 DUPLICATE_APPLICATION when an active application exists.", + "testId": "AEGIS-APP-VAL-008", + "testDescription": "Boundary and negative tests for address schema, employment conditional fields, decimal maxima, sin_consent, CSRF requirements, and duplicate application rules.", + "prerequisites": "Authenticated user with verified email and active session; JWT cookies and X-CSRF-Token available; no existing active application for this user at test start.", + "stepsToPerform": "1. Start Step 1 POST /v2/applications/start with invalid address.postal_code '123 456'; expect 400 with field error for Canadian format A1A 1A1.\n2. Retry with address.province 'Ontario' (not 2-char ISO 3166-2); expect 400 with field error for province.\n3. Retry with id_type 'HEALTH_CARD' (unsupported); expect 400 invalid enum value; correct to DRIVERS_LICENSE with valid id_number.\n4. Submit fully valid Step 1 payload (full_legal_name max length within 100, email matches authenticated user, phone +E.164, valid address); expect 201 with application_id and session_token returned and UI shows draft auto-save enabled.\n5. Immediately call POST /v2/applications/start again with the same user details; expect 409 DUPLICATE_APPLICATION indicating an active application in progress.\n6. Proceed to Step 2: POST /v2/applications/{application_id}/financials with employment_status EMPLOYED but omit employer_name; include X-App-Session and X-CSRF-Token; expect 400 with field error that employer_name is required.\n7. Retry Step 2 with sin_consent false and all other fields valid; expect 400 with error for sin_consent must be true.\n8. Retry Step 2 without X-CSRF-Token (cookies still valid); expect rejection per NFR-05 (403 CSRF_MISSING) and no financial data saved.\n9. Submit Step 2 correctly: employment_status EMPLOYED, employer_name 'Aegis QA', gross_annual_income 9,999,999.99 (max boundary), other_income omitted, monthly_rent 0.00 (allowed), existing_debt_payments 250.00, sin_consent true; include X-App-Session and X-CSRF-Token; expect 200 PENDING_REVIEW with fico_pull_id.\n10. Attempt Step 3 POST /v2/applications/{application_id}/submit without X-CSRF-Token (but with valid e_signature and card_product_id); expect rejection due to CSRF (403 CSRF_MISSING) and no decision created.", + "expectedResult": "Invalid postal code, province, and id_type are rejected with 400; valid Step 1 yields application_id/session_token; duplicate Step 1 returns 409 DUPLICATE_APPLICATION; Step 2 enforces employer_name when EMPLOYED, requires sin_consent true, and rejects missing CSRF with 403; valid Step 2 accepts boundary decimals and returns PENDING_REVIEW; Step 3 without CSRF is rejected and does not finalize decision.", + "apiEndpoint": "POST /v2/applications/start, POST /v2/applications/{application_id}/financials, POST /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , X-App-Session: for Step 2, Content-Type: application/json", + "requestBody": "Step1: full_legal_name, email, phone_number, residential_address (street, city, province, postal_code), id_type, id_number\nStep2: employment_status, employer_name, gross_annual_income, other_income, monthly_rent, existing_debt_payments, sin_consent\nStep3: card_product_id, e_signature", + "responseCodesExpected": "201,400,409,200,403", + "role": "Applicant", + "stateBefore": "No active application for user", + "stateAfter": "One active application in PENDING_REVIEW after valid Step 2; no decision created due to CSRF-blocked Step 3", + "calculationFormula": "", + "mccCode": "", + "currency": "", + "csrfTokenPresent": "Mixed: true on valid calls, false deliberately for CSRF negatives", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE", + "piiMaskingExpected": "No PAN in any responses; PII kept minimal in errors", + "auditLogExpected": "No credit_limit audit until approval; standard application audit entries", + "regulatoryRefs": "NFR-05 CSRF", + "errorCodeExpected": "DUPLICATE_APPLICATION, CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: CSRF failure returns 403 with error code CSRF_MISSING; ASSUMPTION: Duplicate application is detected based on active status" + }, + { + "type": "functional", + "title": "Report lost/stolen irreversible flow with invalid transitions, OTP failures, and PIN format enforcement", + "description": "Validate that reporting lost/stolen irreversibly blocks the card, prevents further status changes, enforces OTP attempt handling on freeze, and PIN set rules return correct errors when card is blocked.", + "testId": "AEGIS-CARD-LOST-009", + "testDescription": "State-transition and negative tests for REQ-007 and REQ-008 including invalid transitions post-block, 401 OTP_FAILED with attempts_remaining, 400 PIN_FORMAT/PIN_MISMATCH, and 409 ALREADY_BLOCKED on duplicate reports.", + "prerequisites": "User logged in with valid JWT and CSRF token; card_id belongs to user and is in Active status; OTP delivery channel functional.", + "stepsToPerform": "1. Navigate to Card Controls; attempt PATCH /v2/cards/{card_id}/status with status 'Frozen' and an invalid confirm_otp '000000'; expect 401 OTP_FAILED with attempts_remaining decremented.\n2. Retry freeze with another invalid OTP; verify attempts_remaining decreases and UI indicates retry limits; do not exceed lock limit (not specified).\n3. Attempt PUT /v2/cards/{card_id}/pin with new_pin '12345' (5 digits), confirm_pin '12345', session_otp valid; expect 400 PIN_FORMAT.\n4. Retry PIN set with new_pin '1234' and confirm_pin '4321' (mismatch); expect 400 PIN_MISMATCH; verify that PIN is not set.\n5. Report card as lost/stolen: POST /v2/cards/{card_id}/report-lost with loss_type 'STOLEN' and last_known_use timestamp; expect 200 with blocked_card_id, new_card_eta, case_number; verify UI banner shows Blocked and replacement scheduled.\n6. Attempt to unfreeze or set status back to 'Active' via PATCH /v2/cards/{card_id}/status with a valid OTP; expect 400 INVALID_TRANSITION and allowed_transitions shows none for Blocked.\n7. Attempt PUT /v2/cards/{card_id}/pin again with valid 4-digit PIN and session_otp; expect 403 CARD_BLOCKED and no change to PIN state.\n8. Attempt to report lost again on the same card_id; expect 409 ALREADY_BLOCKED and no new case created.\n9. Attempt a transaction POST /v2/accounts/{account_id}/transactions after block; expect 403 CARD_INACTIVE with card_status 'Blocked' and no auth_code.\n10. Inspect all responses and UI to ensure PAN is masked if referenced (**** **** **** 1234) and that audit log entries exist for the lost/stolen report and attempted invalid transitions.", + "expectedResult": "Freeze with bad OTP returns 401 OTP_FAILED and decrements attempts_remaining; PIN format and mismatch errors return 400; reporting lost/stolen returns 200 and permanently blocks the card; any subsequent status changes are rejected with 400 INVALID_TRANSITION; PIN set attempts on a blocked card return 403 CARD_BLOCKED; duplicate report returns 409 ALREADY_BLOCKED; transactions are rejected with card_status 'Blocked'; no unmasked PAN in any payloads; audit trail reflects the block event.", + "apiEndpoint": "PATCH /v2/cards/{card_id}/status, POST /v2/cards/{card_id}/report-lost, PUT /v2/cards/{card_id}/pin, POST /v2/accounts/{account_id}/transactions", + "httpMethod": "PATCH, POST, PUT, POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , Content-Type: application/json", + "requestBody": "Card status: status, reason, confirm_otp\nReport lost: loss_type, last_known_use, delivery_address (optional)\nPIN: new_pin, confirm_pin, session_otp\nTransaction: transaction_amount, merchant_name, merchant_id, mcc_code, currency_code, exchange_rate, transaction_type, description", + "responseCodesExpected": "200,400,401,403,409", + "role": "Cardholder", + "stateBefore": "Card Active, no replacement in process", + "stateAfter": "Card Blocked with replacement scheduled; no further status changes allowed; no transactions permitted", + "calculationFormula": "", + "mccCode": "Any valid MCC for step 9 (e.g., 5942 Books)", + "currency": "CAD", + "csrfTokenPresent": "true", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only in any references", + "auditLogExpected": "Lost/Stolen report recorded with case_number; invalid transition attempts logged", + "regulatoryRefs": "REQ-007 Card Controls, REQ-008 PIN, NFR-05 CSRF, PCI-DSS L1", + "errorCodeExpected": "OTP_FAILED, INVALID_TRANSITION, CARD_BLOCKED, ALREADY_BLOCKED, CARD_INACTIVE", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: allowed_transitions for Blocked is empty; ASSUMPTION: OTP attempt limit not specified but attempts_remaining decrements are observable" + }, + { + "type": "functional", + "title": "Transactions validation: FX exchange rate requirement, MCC and merchant_id format, and essential buffer exact boundary", + "description": "Ensure POST /v2/accounts/{account_id}/transactions enforces exchange_rate when currency_code != CAD, validates MCC length and merchant_id length, accepts description at 255 chars, and applies essential over-limit buffer at exactly 5% while rejecting 5.01%. Also covers CASH_ADVANCE and BALANCE_TRANSFER acceptance.", + "testId": "AEGIS-TXN-VAL-010", + "testDescription": "Field validation and boundary tests for transaction initiation including FX parameter dependency, string length constraints, and over-limit buffer edge.", + "prerequisites": "Active account_id owned by user; card status Active; available_credit exactly CAD 1,000.00; valid CSRF token; essential MCCs include 5411 (Grocery) and 4900 (Utilities).", + "stepsToPerform": "1. Attempt USD purchase without exchange_rate: POST /v2/accounts/{account_id}/transactions with currency_code 'USD' and missing exchange_rate; expect 400 with field error indicating exchange_rate required when currency_code != CAD.\n2. Submit with invalid mcc_code '123' (3 digits); expect 400 field error for MCC length/format.\n3. Submit with merchant_id exceeding 32 chars (e.g., 33 'A's); expect 400 field error for merchant_id length.\n4. Submit a valid CAD PURCHASE with description length exactly 255 chars, valid MCC (5942), and amount 10.00; expect 200 approved, transaction_id and auth_code present, description echoed intact.\n5. Test essential buffer at exact boundary: with available_credit 1,000.00, submit MCC 5411 Grocery amount 1,050.00; expect 200 approved with over_limit_flag true and available_credit reflecting buffer usage.\n6. Test essential buffer just above boundary: submit MCC 5411 amount 1,050.01; expect 402 INSUFFICIENT_FUNDS and no change to balance.\n7. Initiate CASH_ADVANCE for amount 50.00 with valid merchant fields and MCC 6011; expect 200 approved if within limits; verify available_credit decreases accordingly.\n8. Initiate BALANCE_TRANSFER for amount 100.00 with description 'BT to Aegis'; expect 200 approved and available_credit updated; confirm transaction_type recorded correctly.\n9. Verify FX fee field presence behavior: for CAD transactions above, response should not include non-zero foreign_fee_amount; confirm no FX fee is applied for CAD.\n10. Confirm all responses mask any PAN references as **** **** **** 1234 and include CSRF requirement enforcement in request processing.", + "expectedResult": "Server requires exchange_rate for non-CAD, rejects invalid MCC and overlong merchant_id with 400; accepts 255-char description; approves essential purchase at exactly 5% buffer with over_limit_flag true, and rejects at 5.01% with 402; CASH_ADVANCE and BALANCE_TRANSFER succeed within available credit; no foreign_fee_amount on CAD; PAN masked in all outputs; CSRF token required on POST.", + "apiEndpoint": "POST /v2/accounts/{account_id}/transactions", + "httpMethod": "POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , Content-Type: application/json", + "requestBody": "transaction_amount, merchant_name, merchant_id, mcc_code, currency_code, exchange_rate, transaction_type, description", + "responseCodesExpected": "200,400,402", + "role": "Cardholder", + "stateBefore": "Account Active with available_credit 1,000.00; no pending holds for this test", + "stateAfter": "Multiple transactions posted as per approvals; balance unchanged for rejected cases; no security violations", + "calculationFormula": "Essential over-limit buffer: approve up to available_credit × 1.05 for essential MCCs", + "mccCode": "5411 Grocery (essential), 5942 Books, 6011 ATM/Financial", + "currency": "CAD, USD", + "csrfTokenPresent": "true", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only", + "auditLogExpected": "Standard transaction audit; no credit_limit changes", + "regulatoryRefs": "REQ-006 FX Fee dependency, NFR-05 CSRF", + "errorCodeExpected": "INSUFFICIENT_FUNDS", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Missing exchange_rate yields 400 validation error; ASSUMPTION: MCC format validation enforced server-side" + }, + { + "type": "functional", + "title": "Notifications webhook validation errors and idempotency scoping across accounts", + "description": "Validate POST /v2/notifications/webhook rejects invalid alert_type/channel and missing idempotency_key, enforces idempotency per account, and delivers valid alerts across channels without PII leakage.", + "testId": "AEGIS-NOTIF-011", + "testDescription": "Negative and boundary tests for notifications webhook including error handling and idempotency behavior across different accounts.", + "prerequisites": "Two active accounts A and B; test harness with service authorization to call internal webhook; in-app notification center visible for account A; SMS/Email/PUSH stubs available.", + "stepsToPerform": "1. POST /v2/notifications/webhook with alert_type 'PAYMENT_REMINDER' (invalid), channel 'IN_APP', valid account_id A, message_body, severity 'INFO', idempotency_key a valid UUID; expect 400 INVALID_ALERT_TYPE and no delivery.\n2. Retry with valid alert_type but invalid channel 'FAX'; expect 400 INVALID_ALERT_TYPE (invalid channel) and no delivery.\n3. Retry with a valid alert_type 'STATEMENT_READY' but omit idempotency_key; expect 400 validation error and no delivery.\n4. Send valid alert: account_id A, alert_type 'STATEMENT_READY', channel 'IN_APP', severity 'INFO', message_body 'Your statement is ready', idempotency_key K1; expect 200 with notification_id and delivered_at; verify in-app toast appears once.\n5. Replay exact same payload with idempotency_key K1; expect 409 DUPLICATE_NOTIFICATION; UI should not duplicate the toast.\n6. Send the same idempotency_key K1 but for account_id B (different account); expect 200 success (idempotency scoped per account); verify delivery to account B only.\n7. Send valid alerts across channels: FRAUD_FLAG (SMS, CRITICAL), OVER_LIMIT (EMAIL, WARNING), PIN_LOCKED (PUSH, WARNING), LATE_PAYMENT (EMAIL, CRITICAL) each with unique idempotency keys; expect 200 for each and exactly one delivery per alert in the respective stubs.\n8. Attempt to call the webhook without required service authorization header/token; expect 401/403 (FORBIDDEN) and no processing.\n9. Inspect delivered payloads in stubs and in-app UI to ensure no PAN or SSN present and only masked identifiers where applicable.\n10. Confirm delivery records show correct channel and timestamp, and that duplicate replay attempts beyond Step 5 continue to yield 409 without side effects.", + "expectedResult": "Invalid alert_type/channel and missing idempotency_key yield 400; valid alert to account A succeeds once with 200 and duplicate replay returns 409; same idempotency_key may be accepted for a different account (per-account scoping); all channel deliveries occur exactly once with 200; unauthorized calls are rejected; no PII leakage in notifications.", + "apiEndpoint": "POST /v2/notifications/webhook", + "httpMethod": "POST", + "requestHeaders": "Authorization: , Content-Type: application/json", + "requestBody": "account_id, alert_type, channel, message_body, severity, idempotency_key", + "responseCodesExpected": "200,400,409,401,403", + "role": "System Integration (Internal Service)", + "stateBefore": "No pending notifications for test accounts; stubs are empty", + "stateAfter": "Valid notifications delivered once per unique idempotency key and account; duplicates rejected", + "calculationFormula": "", + "mccCode": "", + "currency": "", + "csrfTokenPresent": "false", + "authTokenType": "Internal service auth (not end-user JWT)", + "piiMaskingExpected": "No PAN or SSN in any notification payloads or UI; masked identifiers only if included", + "auditLogExpected": "Webhook delivery logs recorded with idempotency_key and channel", + "regulatoryRefs": "NFR-05 applies to portal endpoints; webhook is internal; PCI-DSS L1 data masking", + "errorCodeExpected": "INVALID_ALERT_TYPE, DUPLICATE_NOTIFICATION, FORBIDDEN", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Idempotency scope is per account; ASSUMPTION: Internal webhook does not require CSRF" + }, + { + "type": "functional", + "title": "Email verification initiation, blocked pre-verify login, token expiry/resend, duplicate verification handling", + "description": "Validate end-to-end email verification lifecycle after registration, ensuring login is blocked until verification, expired/malformed token handling, resend flow, and duplicate token reuse protection without PII leakage.", + "testId": "AEGIS-AUTH-VER-012", + "testDescription": "Covers verification link processing and enforcement that unverified users cannot log in; validates resend and duplicate verification behaviors.", + "prerequisites": "Portal reachable, CSRF token available on registration page, unique email such as verify.qa+20260428@example.com, browser devtools for cookie/DOM inspection.", + "stepsToPerform": "1. Navigate to https://portal.aegiscard.com/register and obtain CSRF token.\n2. Submit POST /v2/auth/register with valid fields (first_name 'Verity', last_name 'Quinn', email 'verify.qa+20260428@example.com', password meeting >=12 with classes, date_of_birth '1990-04-28', phone_number '+14165550199', ssn_last4 '1234', agree_terms true); expect 201 and UI shows 'Verification email sent'.\n3. Attempt POST /v2/auth/login with the same email/password before verifying; expect rejection and UI message indicating account not verified.\n4. Click verification link from test harness using the token from registration; call ASSUMPTION endpoint GET /v2/auth/verify-email?token=; expect 200 'VERIFIED' and UI confirmation.\n5. Attempt to reuse the same verification token; expect 409 with error 'ALREADY_VERIFIED' and no state change.\n6. Request resend prior to verification on a fresh new account: register another email 'verify.qa+20260428b@example.com' validly (201), then call ASSUMPTION endpoint POST /v2/auth/verification/resend with email; expect 200 and new token issued.\n7. Use an intentionally malformed/expired verification token string 'abc.def.ghi' on GET /v2/auth/verify-email; expect 400 with error 'INVALID_TOKEN' or 'TOKEN_EXPIRED' and no verification.\n8. Verify the resent valid token by calling GET /v2/auth/verify-email?token=; expect 200 'VERIFIED'.\n9. After successful verification, login via POST /v2/auth/login with valid mfa_code if prompted; expect 200 and JWT cookies set as HttpOnly/Secure; confirm no tokens in localStorage/sessionStorage.\n10. Inspect responses and UI to ensure no SSN beyond masked ***-**-1234 and no PAN present; verify SameSite=Strict on cookies.", + "expectedResult": "Unverified login is blocked until email verification completes; valid token verifies the account; duplicate verification attempts return 409; malformed/expired tokens return 400 and do not verify; resend issues a new token that verifies successfully; upon verification, login succeeds with JWT in HttpOnly/Secure cookies and no token storage in localStorage; no PII leaks.", + "apiEndpoint": "POST /v2/auth/register\nPOST /v2/auth/login\nGET /v2/auth/verify-email (ASSUMPTION)\nPOST /v2/auth/verification/resend (ASSUMPTION)", + "httpMethod": "POST, POST, GET, POST", + "requestHeaders": "Content-Type: application/json, X-CSRF-Token: (registration), Cookie: HttpOnly Secure JWT cookies (login)", + "requestBody": "Register: first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms\nLogin: email,password,mfa_code (if required)\nResend: email", + "responseCodesExpected": "201,200,400,401,403,409", + "role": "Prospective Applicant", + "stateBefore": "Email not registered; no session", + "stateAfter": "Primary account verified and able to log in; secondary account verified via resend; no tokens in localStorage", + "calculationFormula": "", + "mccCode": "", + "currency": "", + "csrfTokenPresent": "true for registration and resend; not required for GET verify link per ASSUMPTION", + "authTokenType": "OAuth 2.0 PKCE with JWT cookies after login", + "piiMaskingExpected": "SSN ***-**-1234 only; no PAN present anywhere", + "auditLogExpected": "Standard auth events only; no credit_limit changes", + "regulatoryRefs": "PCI-DSS L1,NFR-05 CSRF,NFR-06 Session Timeout", + "errorCodeExpected": "ACCOUNT_UNVERIFIED (ASSUMPTION), ALREADY_VERIFIED, INVALID_TOKEN, TOKEN_EXPIRED", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Login is blocked for unverified accounts returning 403 ACCOUNT_UNVERIFIED; ASSUMPTION: Verification endpoints exist as /v2/auth/verify-email and /v2/auth/verification/resend; ASSUMPTION: Times and token expiry evaluated in UTC." + }, + { + "type": "functional", + "title": "Refresh token rotation under multi-tab concurrency with reuse rejection and CSRF continuity", + "description": "Ensure single-use refresh tokens behave correctly when two tabs attempt refresh concurrently, cookies propagate latest tokens, old refresh reuse is rejected, and CSRF remains enforced post-rotation.", + "testId": "AEGIS-AUTH-REF-013", + "testDescription": "Simulate two browser tabs sharing cookies to validate refresh rotation, reuse rejection, and uninterrupted session continuity.", + "prerequisites": "Existing verified user with MFA; browser capable of opening two tabs sharing same cookie jar; CSRF token obtainable from portal bootstrap.", + "stepsToPerform": "1. In Tab A, POST /v2/auth/login with correct email/password and valid mfa_code, device_id UUIDv4, remember_me true; expect 200 with access_token and refresh_token R1 set in HttpOnly/Secure cookies.\n2. Open Tab B to the portal; confirm cookies reflect R1; ensure no tokens stored in localStorage/sessionStorage.\n3. In Tab A, call POST /v2/auth/token/refresh using R1; expect 200 with new access_token and refresh_token R2; verify cookies now contain R2.\n4. Nearly simultaneously in Tab B, call POST /v2/auth/token/refresh presenting R1; expect 401 TOKEN_INVALID due to rotation; ensure UI surfaces a silent retry path.\n5. In Tab B, issue a protected GET (e.g., GET /v2/accounts/{account_id}/summary) using current cookies (now with R2) to confirm session remains valid; expect 200.\n6. In Tab B, explicitly call POST /v2/auth/token/refresh using R2; expect 200 with refresh_token R3; verify cookies rotated to R3.\n7. Attempt to reuse R2 in either tab to call refresh again; expect 401 TOKEN_INVALID, confirming single-use enforcement.\n8. Perform a state-changing POST (e.g., POST /v2/accounts/{account_id}/transactions for a $1.00 test purchase) without X-CSRF-Token after rotation; expect 403 CSRF_MISSING and no transaction created.\n9. Retry the same POST including valid X-CSRF-Token; expect 200 and transaction_id returned; confirm SameSite=Strict remains set on cookies after refresh.", + "expectedResult": "First refresh rotates R1 to R2; concurrent refresh with R1 returns 401; both tabs continue authenticated using the latest cookies; subsequent rotation to R3 succeeds; any reuse of prior refresh tokens returns 401; CSRF remains enforced post-rotation; no tokens stored in localStorage; cookies are HttpOnly, Secure, SameSite=Strict.", + "apiEndpoint": "POST /v2/auth/login\nPOST /v2/auth/token/refresh\nGET /v2/accounts/{account_id}/summary\nPOST /v2/accounts/{account_id}/transactions", + "httpMethod": "POST, POST, GET, POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Login: email,password,mfa_code,device_id,remember_me\nTransactions: transaction_amount, merchant_name, merchant_id, mcc_code, transaction_type 'PURCHASE', description", + "responseCodesExpected": "200,401,403", + "role": "Cardholder", + "stateBefore": "User account exists and is verified; no active session", + "stateAfter": "Session active in both tabs with latest rotated tokens; one small transaction created via CSRF-compliant call", + "calculationFormula": "", + "mccCode": "5942 Books (for small test purchase)", + "currency": "CAD", + "csrfTokenPresent": "false for negative test, true on retry", + "authTokenType": "JWT in HttpOnly, Secure cookies; refresh rotation single-use", + "piiMaskingExpected": "PAN **** **** **** 1234 only if referenced; no raw PAN in DOM or network", + "auditLogExpected": "Standard auth session events; no credit_limit audit changes", + "regulatoryRefs": "NFR-05 CSRF,NFR-06 Session Timeout,PCI-DSS L1", + "errorCodeExpected": "TOKEN_INVALID, CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Tabs share cookie jar; ASSUMPTION: Transaction test purchase is allowed at $1.00; ASSUMPTION: CSRF failure returns 403 CSRF_MISSING." + }, + { + "type": "functional", + "title": "Transactions history filters: category coverage, date-range validation, and pagination extremes", + "description": "Validate GET /v2/accounts/{account_id}/transactions supports category filtering for PURCHASE, REFUND, FEE, INTEREST, enforces date range rules, and handles pagination boundaries.", + "testId": "AEGIS-TXN-HIST-014", + "testDescription": "Exhaustively tests history query behavior across categories and boundary paging, including invalid date range rejection.", + "prerequisites": "Active account with a populated billing cycle including at least: 10 PURCHASE, 2 REFUND, 1 FEE, 1 INTEREST transactions spread across the current and previous month; valid JWT and CSRF token (CSRF not required for GET).", + "stepsToPerform": "1. Seed data via test harness to ensure required transactions across categories and dates exist; confirm posted dates span two calendar months.\n2. Issue GET /v2/accounts/{account_id}/transactions with no query params; expect 200 with default cycle range and pagination defaults (page=1, per_page=25) showing a mix of categories.\n3. Call GET with category=PURCHASE; expect only PURCHASE items returned and count equals seeded purchases within date window.\n4. Call GET with category=REFUND; verify only refund entries, amounts and signs consistent with system convention, and no PURCHASE entries present.\n5. Call GET with from_date later than to_date (e.g., from_date=2026-05-10, to_date=2026-05-01); expect 400 INVALID_DATE_RANGE and no data returned.\n6. Call GET with from_date and to_date bounding just the previous month; expect 200 with only transactions in that window.\n7. Call GET with per_page=100, page=1; expect up to 100 items and total_pages >= 1 reported; validate page and per_page echo.\n8. Determine total_pages from response; request page=total_pages+1; expect 200 with empty transactions[] and page metadata reflecting requested page.\n9. Request page=0 (invalid min); expect 400 validation error for page minimum 1 and no results returned.\n10. Inspect all payloads to ensure any card references are masked as **** **** **** 1234 and that no PII (PAN/SSN) is present.", + "expectedResult": "Category filter correctly scopes results; invalid date ranges return 400; bounded date queries return only in-range items; per_page accepts up to 100; requesting beyond last page returns empty set with valid metadata; page=0 is rejected; all responses mask PAN and contain no PII leakage.", + "apiEndpoint": "GET /v2/accounts/{account_id}/transactions", + "httpMethod": "GET", + "requestHeaders": "Authorization: Bearer , Content-Type: application/json", + "requestBody": "", + "responseCodesExpected": "200,400", + "role": "Cardholder", + "stateBefore": "Account has seeded transactions across categories and months", + "stateAfter": "No state change; data retrieved only", + "calculationFormula": "", + "mccCode": "Various MCCs across categories; not material to history query", + "currency": "CAD", + "csrfTokenPresent": "false (GET endpoint)", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only, no SSN", + "auditLogExpected": "Read access logs only; no mutations", + "regulatoryRefs": "PCI-DSS L1", + "errorCodeExpected": "INVALID_DATE_RANGE", + "paginationParams": "page=1..n, per_page up to 100", + "featureFlag": "", + "assumptions": "ASSUMPTION: Out-of-range pages return 200 with empty list; ASSUMPTION: Default date window equals current billing cycle." + }, + { + "type": "functional", + "title": "Real-time stream: authorized subscription to own account, forbidden cross-account subscribe, schema validation, and reconnect without duplicates", + "description": "Validate WebSocket stream authentication, topic-level authorization, message schema handling, heartbeat, and reconnect behavior ensuring no duplicate events.", + "testId": "AEGIS-WS-STREAM-015", + "testDescription": "Tests that the client can subscribe to their own account feed only, receives well-formed messages, and handles disconnects and reconnects cleanly.", + "prerequisites": "Verified user with active account_id; backend test publisher capable of emitting transaction events with message_id; browser supports WebSocket; valid JWT available.", + "stepsToPerform": "1. Log in via POST /v2/auth/login and obtain JWT cookies; confirm SameSite=Strict and no localStorage token storage.\n2. Connect to wss://realtime.aegiscard.com/v2/stream using JWT for auth (ASSUMPTION: Authorization header or subprotocol); expect server 'connected' ack.\n3. Subscribe to topic 'account:{account_id}' (ASSUMPTION: client sends a subscribe frame with topic name); expect 'subscribed' confirmation.\n4. Using publisher, emit a test event for this account with fields message_id, type 'transaction.authorized', amount 42.50, mcc 5942, masked_pan '**** **** **** 1234'; verify UI feed displays event and all required fields are present and correctly rendered.\n5. Attempt to subscribe to 'account:{other_account_id}' not owned by the user; expect server error 'SUBSCRIPTION_FORBIDDEN' or equivalent and no data delivered for that topic.\n6. Emit an intentionally malformed event (missing amount or invalid type) via publisher; verify client ignores or logs validation error without crashing (ASSUMPTION: client-side schema validation enforced).\n7. Simulate transient network drop by disconnecting the socket; verify client initiates reconnect with backoff and resubscribes to 'account:{account_id}'.\n8. After reconnect, emit the same event again with the same message_id as before and a new event; verify client deduplicates based on message_id and shows only one instance of the repeated event and the new event once.\n9. Observe heartbeat/ping frames; simulate missed heartbeat to force server-initiated close; ensure client reconnects and resumes without loss; confirm no PAN or SSN beyond masked values in any payloads.", + "expectedResult": "Client subscribes only to own account stream, forbidden cross-account subscription is rejected; well-formed messages update UI; malformed messages are ignored gracefully; reconnect logic restores subscription without duplicate events; heartbeat failures trigger reconnect; all payloads mask PAN.", + "apiEndpoint": "wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "WS", + "requestHeaders": "Authorization: Bearer (ASSUMPTION via header or subprotocol), Sec-WebSocket-Protocol as needed", + "requestBody": "Subscribe frame: topic 'account:{account_id}' (ASSUMPTION); messages contain message_id,type,amount,mcc,masked_pan", + "responseCodesExpected": "101 Switching Protocols (implicit), application-level acks/errors", + "role": "Cardholder", + "stateBefore": "No active WebSocket connection", + "stateAfter": "Active subscription to own account feed; successful reconnects; no unauthorized data received", + "calculationFormula": "", + "mccCode": "5942 Books for sample event", + "currency": "CAD", + "csrfTokenPresent": "false (WS is not CSRF-protected; auth via JWT at handshake)", + "authTokenType": "JWT in HttpOnly cookies used to authorize WS per ASSUMPTION", + "piiMaskingExpected": "masked_pan present as **** **** **** 1234 only; no full PAN", + "auditLogExpected": "Connection/subscription events logged for monitoring; no credit_limit audits", + "regulatoryRefs": "PCI-DSS L1", + "errorCodeExpected": "SUBSCRIPTION_FORBIDDEN (application-level), none at protocol level", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Topic naming and subscribe frames follow 'account:{id}' convention; ASSUMPTION: message_id enables client dedupe; ASSUMPTION: WS auth uses JWT in header or subprotocol." + }, + { + "type": "functional", + "title": "Payment scheduling boundary validations: min $1.00, past-date rejection, same-day immediate, and FULL_BALANCE max", + "description": "Validate POST /v2/accounts/{account_id}/payments enforces minimum amount, rejects past scheduled_date, treats same-day as immediate, and accepts FULL_BALANCE equal to total_balance with UTC handling.", + "testId": "AEGIS-PAY-SCHED-016", + "testDescription": "Covers key boundary conditions for payments scheduling and amount limits including timezone considerations.", + "prerequisites": "Active account with total_balance > 0 and known minimum_payment_due; at least one valid linked bank_account_id; valid JWT and X-CSRF-Token; test clock in UTC.", + "stepsToPerform": "1. Retrieve account summary to capture current total_balance and minimum_payment_due from GET /v2/accounts/{account_id}/summary; verify 200.\n2. Attempt POST /v2/accounts/{account_id}/payments with payment_amount 0.99, payment_type 'CUSTOM', valid bank_account_id; expect 400 for amount below $1.00 minimum.\n3. Attempt POST with scheduled_date set to yesterday in UTC (past date) and payment_amount 5.00; expect 400 validation error 'SCHEDULED_DATE_INVALID' and no payment created.\n4. Attempt POST with scheduled_date equal to today (UTC) and payment_amount 5.00; expect system to treat as immediate or reject as not future; ASSUMPTION: treated as immediate with scheduled_date omitted in response.\n5. Submit POST with payment_type 'FULL_BALANCE' and payment_amount exactly equal to captured total_balance; expect 200 accepted with new_balance_estimate 0.00.\n6. Submit POST with payment_type 'CUSTOM' and payment_amount equal to minimum_payment_due minus $0.01; expect 400 BELOW_MINIMUM referencing minimum_payment_due.\n7. Submit a valid future-dated scheduled payment 7 days ahead in UTC with payment_amount 10.00; expect 200 with scheduled_date echoed exactly in UTC format.\n8. Verify CSRF enforcement by attempting a repeat POST without X-CSRF-Token; expect 403 CSRF_MISSING and no duplicate payment.\n9. Inspect all responses to confirm no PAN present and that masked data appears only where appropriate; confirm SameSite=Strict cookies persist across the flow.", + "expectedResult": "Payments below $1.00 are rejected; past scheduled_date is rejected; same-day scheduled_date is handled per assumption as immediate; FULL_BALANCE equal to total_balance is accepted with new_balance_estimate 0.00; CUSTOM below minimum triggers 400 BELOW_MINIMUM; future scheduled payment is accepted with correct UTC date; missing CSRF triggers 403 and no duplicate; no PII leakage.", + "apiEndpoint": "GET /v2/accounts/{account_id}/summary\nPOST /v2/accounts/{account_id}/payments", + "httpMethod": "GET, POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: for POST, Content-Type: application/json", + "requestBody": "Payments: payment_amount, payment_type {MINIMUM, STATEMENT_BALANCE, CUSTOM, FULL_BALANCE}, bank_account_id, scheduled_date (ISO 8601 UTC)", + "responseCodesExpected": "200,400,403", + "role": "Cardholder", + "stateBefore": "Account has outstanding balance and linked bank account; no payments queued for the chosen dates", + "stateAfter": "One immediate FULL_BALANCE payment posted, one future scheduled payment queued; invalid attempts rejected without side effects", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for valid posts; false for deliberate negative", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "No PAN in payment responses; masked identifiers only if any card refs are included", + "auditLogExpected": "Standard payment submission logs; no credit_limit audit", + "regulatoryRefs": "NFR-05 CSRF,PCI-DSS L1", + "errorCodeExpected": "BELOW_MINIMUM, CSRF_MISSING, SCHEDULED_DATE_INVALID (ASSUMPTION)", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Amount < $1.00 returns 400; ASSUMPTION: Same-day scheduled_date is treated as immediate; ASSUMPTION: All times evaluated in UTC." + }, + { + "type": "functional", + "title": "Trusted device and remember_me TTL: 30-day refresh, MFA prompt suppression on known device", + "description": "Validate that remember_me=true extends refresh token TTL to 30 days and that a known device_id reduces MFA prompts on subsequent logins while maintaining security controls.", + "testId": "AEGIS-AUTH-TRUST-017", + "testDescription": "Covers device trust via device_id, remember_me 30-day refresh TTL, MFA challenge behavior on known vs new devices, cookie flags, and post-login CSRF usage.", + "prerequisites": "Existing verified user with MFA enabled; ability to observe Set-Cookie attributes; two logical devices identified by distinct UUIDv4 device_id values D1 and D2; browser/portal accessible over TLS 1.3.", + "stepsToPerform": "1. From Device D1, call POST /v2/auth/login with email, password, remember_me true, device_id D1 and omit mfa_code; expect 401 INVALID_CREDENTIALS or MFA prompt; supply correct 6-digit mfa_code and resubmit; expect 200 with access_token and refresh_token in HttpOnly, Secure, SameSite=Strict cookies.\n2. Inspect response Set-Cookie for refresh_token on D1; verify Expires/Max-Age reflects approximately 30 days TTL when remember_me=true; confirm no tokens exist in localStorage or sessionStorage.\n3. Close the session (browser restart simulation). From Device D1, call POST /v2/auth/login with email, password, remember_me true, device_id D1 and omit mfa_code; expect 200 success without MFA prompt due to trusted device recognition; verify new access/refresh tokens issued.\n4. Confirm cookies are HttpOnly, Secure, SameSite=Strict and that CSRF token is available from portal bootstrap for state-changing requests.\n5. From Device D2, call POST /v2/auth/login with email, password, remember_me false, device_id D2 and omit mfa_code; expect MFA prompt; submit valid mfa_code; expect 200 with tokens; inspect refresh token cookie TTL is shorter than 30 days (ASSUMPTION: default ≤ 7 days when remember_me=false).\n6. On Device D1, call POST /v2/auth/token/refresh using current refresh_token; expect 200 with rotated pair; immediately retry refresh with the old refresh_token to validate single-use rotation; expect 401 TOKEN_INVALID.\n7. Using the valid session on D1, perform a benign state-changing action with CSRF: POST /v2/accounts/{account_id}/transactions for a $1.00 PURCHASE (MCC 5942) including X-CSRF-Token; expect 200 with transaction_id and masked PAN if referenced.\n8. Attempt the same POST without X-CSRF-Token; expect rejection per NFR-05 (ASSUMPTION: 403 CSRF_MISSING) and no duplicate transaction.\n9. Verify no PII leakage in network/console logs and that all PAN references are masked as **** **** **** 1234; confirm SameSite=Strict continues to be enforced on cookies across these flows.", + "expectedResult": "remember_me=true yields a 30-day refresh token TTL; subsequent login from the same device_id does not prompt MFA (trusted device), whereas a new device requires MFA; refresh tokens rotate and older tokens are rejected; CSRF is required for state-changing requests; cookies remain HttpOnly, Secure, SameSite=Strict; no tokens in localStorage; no unmasked PII.", + "apiEndpoint": "POST /v2/auth/login\nPOST /v2/auth/token/refresh\nPOST /v2/accounts/{account_id}/transactions", + "httpMethod": "POST, POST, POST", + "requestHeaders": "Content-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict (set by server)\nX-CSRF-Token: for valid state-changing POST", + "requestBody": "Login: email,password,mfa_code,device_id (UUIDv4),remember_me\nTransaction: transaction_amount=1.00, merchant_name, merchant_id, mcc_code=5942, transaction_type=PURCHASE, description", + "responseCodesExpected": "200,401,403", + "role": "Cardholder", + "stateBefore": "User exists, MFA enabled, no active session on devices D1 and D2", + "stateAfter": "Active sessions established on D1 and D2 with respective refresh TTLs; one small transaction posted from D1; no security policy violations", + "calculationFormula": "", + "mccCode": "5942 Books", + "currency": "CAD", + "csrfTokenPresent": "true for valid transaction POST, false for deliberate negative", + "authTokenType": "OAuth 2.0 PKCE; JWT in HttpOnly, Secure cookies; refresh rotation enforced", + "piiMaskingExpected": "PAN **** **** **** 1234 only; no full PAN or SSN in DOM or responses", + "auditLogExpected": "Standard auth/session logs; transaction audit for the $1 purchase", + "regulatoryRefs": "PCI-DSS L1,NFR-05 CSRF,NFR-06 Session Timeout", + "errorCodeExpected": "INVALID_CREDENTIALS, TOKEN_INVALID, CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: remember_me=true extends refresh TTL to ~30 days; ASSUMPTION: trusted device_id reduces MFA prompts on subsequent logins; ASSUMPTION: Default refresh TTL when remember_me=false is shorter (e.g., ≤7 days)." + }, + { + "type": "functional", + "title": "Right to rescind on exact Day 14 with CSRF enforcement and post-closure behavior", + "description": "Validate DELETE /v2/accounts/{id} approves rescission exactly at Day 14 boundary, enforces CSRF, and disables subsequent account/card operations with proper audit logging.", + "testId": "AEGIS-RESCIND-BOUND-018", + "testDescription": "Covers boundary condition at Day 14, CSRF requirement on DELETE, and verifies system state and audit trail after account closure.", + "prerequisites": "Account issued exactly 14 days ago (UTC) with no membership fee; user is owner and can log in; CSRF token obtainable; card currently Active.", + "stepsToPerform": "1. Log in via portal and ensure JWT cookies are set; obtain X-CSRF-Token from portal bootstrap; confirm SameSite=Strict cookies and no tokens in localStorage.\n2. Attempt DELETE /v2/accounts/{id} without X-CSRF-Token; expect rejection per NFR-05 (ASSUMPTION: 403 CSRF_MISSING) and account remains active.\n3. Retry DELETE /v2/accounts/{id} including X-CSRF-Token; expect 200 or 204 indicating rescind processed with no membership fee per REQ-016.\n4. Navigate to Dashboard and request GET /v2/accounts/{id}/summary; expect 200 with account_status 'Closed' (ASSUMPTION) and no sensitive data beyond masked identifiers.\n5. Attempt POST /v2/accounts/{account_id}/transactions to initiate any purchase; include CSRF; expect 403 FORBIDDEN or 403 CARD_INACTIVE due to closed account (ASSUMPTION: gateway denies on closed accounts).\n6. Attempt PATCH /v2/cards/{card_id}/status with status 'Frozen' and valid confirm_otp; expect 400 INVALID_TRANSITION with allowed_transitions empty (ASSUMPTION: closed accounts disallow status changes).\n7. Attempt POST /v2/accounts/{account_id}/payments with any valid body; include CSRF; expect 403 FORBIDDEN and no payment queued.\n8. Verify immutable audit trail (NFR-04) records the rescind event with user_id, session_id, ip_address, timestamp_utc and no PAN details in the audit payload.\n9. Inspect all responses and UI after closure for PCI masking: any PAN references appear as **** **** **** 1234; no unmasked PII present.\n10. Attempt to call DELETE /v2/accounts/{id} again post-closure; expect idempotent no-op or 403/409 (ASSUMPTION: 409 ALREADY_CLOSED) with no further state change.", + "expectedResult": "On exact Day 14, rescind with CSRF succeeds and closes the account; missing CSRF is rejected; after closure, account summary reflects 'Closed' and all state-changing operations (transactions, payments, card status changes) are disallowed; audit log records rescind with required fields; no PAN is exposed.", + "apiEndpoint": "DELETE /v2/accounts/{id}\nGET /v2/accounts/{id}/summary\nPOST /v2/accounts/{account_id}/transactions\nPATCH /v2/cards/{card_id}/status\nPOST /v2/accounts/{account_id}/payments", + "httpMethod": "DELETE, GET, POST, PATCH, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: for state-changing requests\nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Transactions: standard fields if attempted\nCard status: status,reason,confirm_otp\nPayments: payment_amount,payment_type,bank_account_id", + "responseCodesExpected": "200,204,400,403", + "role": "Cardholder", + "stateBefore": "Account active with issuance date at Day 14 boundary; card Active; no closure initiated", + "stateAfter": "Account closed; card controls disabled; no transactions or payments allowed; audit trail updated", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for valid DELETE and other state-changing calls; false for deliberate negative in Step 2", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only, no SSN or PAN exposure", + "auditLogExpected": "Rescind recorded immutably with user_id,session_id,ip_address,timestamp_utc", + "regulatoryRefs": "REQ-016 Right to Rescind,NFR-05 CSRF,NFR-04 Audit,PCI-DSS L1", + "errorCodeExpected": "CSRF_MISSING, FORBIDDEN, INVALID_TRANSITION, ALREADY_CLOSED (ASSUMPTION)", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: GET summary returns account_status 'Closed' after rescind; ASSUMPTION: Closed accounts reject transactions/payments and card status changes; ASSUMPTION: Repeat rescind returns 409 ALREADY_CLOSED or a safe no-op." + }, + { + "type": "functional", + "title": "Report lost/stolen with delivery address override validation and replacement confirmation", + "description": "Validate delivery_address override schema for POST /v2/cards/{card_id}/report-lost and confirm replacement scheduling uses the override; ensure masking and audit integrity.", + "testId": "AEGIS-CARD-DELIV-019", + "testDescription": "Focuses on valid and invalid delivery_address override inputs, successful block and replacement scheduling, and post-block restrictions on transactions.", + "prerequisites": "User logged in with valid JWT and X-CSRF-Token; card_id in Active state; ability to verify UI banner and replacement details; test harness time in UTC.", + "stepsToPerform": "1. Attempt POST /v2/cards/{card_id}/report-lost with loss_type 'LOST' and delivery_address override containing province 'Ontario' (non-ISO 3166-2) and a valid last_known_use; include X-CSRF-Token; expect 400 field validation error for province.\n2. Retry with province 'ON' but postal_code '123 456' (invalid for Canadian format); expect 400 field error for postal_code.\n3. Submit a fully valid override: street '100 King St W', city 'Toronto', province 'ON', postal_code 'M5H 1J9', loss_type 'STOLEN', and a valid ISO 8601 UTC last_known_use; include X-CSRF-Token; expect 200 with blocked_card_id, new_card_eta, case_number.\n4. In the portal, verify the Card Controls banner shows 'Blocked' and that replacement shipment address displays the provided override values exactly (no truncation), with no PAN display beyond masked **** **** **** 1234.\n5. Confirm the replacement workflow is scheduled by checking UI or a status field in the response (e.g., new_card_eta not null) and that no membership or replacement fee is displayed (product policy dependent).\n6. Attempt to initiate a transaction via POST /v2/accounts/{account_id}/transactions after block; expect 403 CARD_INACTIVE with card_status 'Blocked' and no auth_code; available_credit remains unchanged by the attempted transaction.\n7. Attempt to PATCH /v2/cards/{card_id}/status back to 'Active' with a valid confirm_otp; expect 400 INVALID_TRANSITION with allowed_transitions empty (irreversible per REQ-007).\n8. Inspect all responses and network logs for PCI masking: any PAN references are in masked form only; verify no PII leakage in error messages.\n9. Verify audit trail contains an entry for the lost/stolen report including case_number and timestamp_utc; ensure address override is not stored or surfaced beyond necessary operational logs (masked in UI).\n10. Optionally, resubmit POST /v2/cards/{card_id}/report-lost with the same card_id to confirm duplicate handling remains idempotent for already Blocked state (ASSUMPTION: 409 ALREADY_BLOCKED without creating a new case).", + "expectedResult": "Invalid province and postal_code are rejected; a valid delivery_address override results in 200 with the card blocked and replacement scheduled; UI shows the override address; transactions are rejected with card_status 'Blocked'; status changes are invalid; masking and audit requirements are met.", + "apiEndpoint": "POST /v2/cards/{card_id}/report-lost\nPOST /v2/accounts/{account_id}/transactions\nPATCH /v2/cards/{card_id}/status", + "httpMethod": "POST, POST, PATCH", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json", + "requestBody": "Report lost: loss_type {LOST, STOLEN}, last_known_use (ISO 8601 UTC), delivery_address {street, city, province (2-char), postal_code (A1A 1A1)}\nTransactions: standard fields\nCard status: status, confirm_otp", + "responseCodesExpected": "200,400,403,409", + "role": "Cardholder", + "stateBefore": "Card Active with no replacement scheduled; no block in place", + "stateAfter": "Card Blocked with replacement scheduled to override address; no further status changes or transactions permitted", + "calculationFormula": "", + "mccCode": "Any for attempted transaction (e.g., 5942)", + "currency": "CAD", + "csrfTokenPresent": "true for all state-changing calls in this test", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only; address displayed as entered without PAN exposure", + "auditLogExpected": "Lost/Stolen block recorded with case_number and timestamp_utc", + "regulatoryRefs": "REQ-007 Card Controls,REQ-014 Data Masking,NFR-05 CSRF,PCI-DSS L1", + "errorCodeExpected": "INVALID_TRANSITION, CARD_INACTIVE, ALREADY_BLOCKED", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: delivery_address override schema follows Step 1 address rules; ASSUMPTION: Duplicate report on already blocked card yields 409 ALREADY_BLOCKED." + }, + { + "type": "functional", + "title": "Refresh token TTL expiry: 401 on expired refresh with re-authentication and CSRF continuity", + "description": "Validate behavior when refresh_token has expired: API returns 401 TOKEN_INVALID, protected endpoints require re-auth, and CSRF enforcement continues post re-login.", + "testId": "AEGIS-AUTH-EXPIRE-020", + "testDescription": "Simulates refresh token expiry, ensures proper invalidation and recovery via login, and verifies cookies and CSRF behavior remain correct.", + "prerequisites": "Verified user with MFA enabled; ability to fast-forward test clock to exceed refresh token TTL; portal reachable over TLS 1.3.", + "stepsToPerform": "1. Call POST /v2/auth/login with email, password, valid mfa_code, remember_me false and device_id set; expect 200 with access_token and refresh_token R1 in HttpOnly, Secure, SameSite=Strict cookies; confirm no tokens in localStorage.\n2. Note the default refresh TTL from Set-Cookie (Expires/Max-Age); fast-forward test clock beyond the TTL to ensure R1 expires (ASSUMPTION: test harness can adjust time in UTC).\n3. Attempt POST /v2/auth/token/refresh using expired R1; expect 401 TOKEN_INVALID; verify no new tokens are issued and cookies remain unchanged.\n4. Attempt GET /v2/accounts/{account_id}/summary with the existing (likely expired) access token; expect 401 INVALID_SESSION or equivalent requiring re-authentication (ASSUMPTION: access token also expired due to 15-minute lifetime).\n5. Re-login via POST /v2/auth/login with valid credentials and mfa_code; expect 200 with fresh tokens R2; verify cookies HttpOnly, Secure, SameSite=Strict and CSRF token loaded by the portal.\n6. Immediately call POST /v2/auth/token/refresh with R2 to confirm normal rotation path works; expect 200 with new pair (R3), and that R2 cannot be reused thereafter (retry should return 401 TOKEN_INVALID).\n7. Attempt a state-changing POST (e.g., POST /v2/accounts/{account_id}/payments for a small $1.00 CUSTOM payment) without X-CSRF-Token; expect 403 CSRF_MISSING and no payment created.\n8. Retry the same payment POST including a valid X-CSRF-Token; expect 200 accepted/queued with payment_id and new_balance_estimate fields present.\n9. Inspect all responses and browser network logs to ensure no unmasked PII and that PAN is never present; verify cookies maintain SameSite=Strict across the flow.", + "expectedResult": "Expired refresh_token returns 401 TOKEN_INVALID; protected endpoints fail until re-login; after re-auth, refresh rotation operates correctly; CSRF is enforced on state-changing requests; cookies remain HttpOnly, Secure, SameSite=Strict; no PII leakage.", + "apiEndpoint": "POST /v2/auth/login\nPOST /v2/auth/token/refresh\nGET /v2/accounts/{account_id}/summary\nPOST /v2/accounts/{account_id}/payments", + "httpMethod": "POST, POST, GET, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: for valid state-changing POSTs\nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Login: email,password,mfa_code,remember_me=false,device_id\nPayments: payment_amount=1.00,payment_type=CUSTOM,bank_account_id", + "responseCodesExpected": "200,401,403", + "role": "Cardholder", + "stateBefore": "User has no active session; refresh token not yet issued", + "stateAfter": "User re-authenticated with fresh tokens; one small payment queued via CSRF-compliant request", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "false for deliberate negative, true for valid payment", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE; refresh rotation single-use", + "piiMaskingExpected": "No PAN in any payment or auth responses; masked identifiers only", + "auditLogExpected": "Standard auth and payment submission logs; no credit_limit audit changes", + "regulatoryRefs": "NFR-05 CSRF,NFR-06 Session Timeout,PCI-DSS L1", + "errorCodeExpected": "TOKEN_INVALID, CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Test harness can simulate refresh TTL expiry; ASSUMPTION: Access token TTL is 15 minutes and also expired before Step 4." + }, + { + "type": "functional", + "title": "Account summary include_rewards toggle and rewards floor verification", + "description": "Validate GET /v2/accounts/{account_id}/summary returns core fields, enforces owner-only access, and controls points_balance via include_rewards while rewards reflect floor rounding rules.", + "testId": "AEGIS-SUMMARY-REW-021", + "testDescription": "Confirms include_rewards parameter behavior and rewards calculation visibility, with masking and authorization checks.", + "prerequisites": "Active account with sufficient available_credit; ability to post at least two transactions: one Travel MCC 4722 and one non-Travel MCC 5942 in CAD; user logged in with valid JWT.", + "stepsToPerform": "1. Post a Travel PURCHASE: POST /v2/accounts/{account_id}/transactions with amount 88.88 CAD, merchant_name 'Travel QA', merchant_id 'TRV8888', mcc_code 4722, transaction_type 'PURCHASE'; include X-CSRF-Token; expect 200 approved.\n2. Post a non-Travel PURCHASE: amount 19.99 CAD, merchant_name 'Book QA', merchant_id 'BK1999', mcc_code 5942, transaction_type 'PURCHASE'; include CSRF; expect 200 approved.\n3. Compute expected rewards using floor-only rules: Travel points = floor(88.88 × 3) = 266; Other points = floor(19.99 × 1) = 19; total incremental points = 285.\n4. Call GET /v2/accounts/{account_id}/summary without include_rewards param (defaults to false); expect 200 with fields current_balance, available_credit, credit_limit, account_status, billing_cycle_end and points_balance omitted (ASSUMPTION: hidden unless include_rewards=true).\n5. Call GET /v2/accounts/{account_id}/summary?include_rewards=true; expect 200 with points_balance present and increased by at least 285 relative to pre-test baseline (or exactly 285 if starting from 0); verify floor rounding (no rounding up).\n6. Attempt GET /v2/accounts/{other_account_id}/summary as the same user; expect 403 FORBIDDEN and no data leakage.\n7. Validate that all responses contain no unmasked PAN and that any card references are masked as **** **** **** 1234 per REQ-014.\n8. Optionally cross-check rewards on the next statement: GET /v2/accounts/{account_id}/statements/{statement_id} when available and verify rewards_earned includes these points (ASSUMPTION: statement generated after cycle ends).\n9. Attempt GET /v2/accounts/{account_id}/summary?include_rewards=yes (invalid boolean); expect server to ignore and default to include_rewards=false (ASSUMPTION) returning no points_balance field.\n10. Confirm SameSite=Strict cookie policy and TLS 1.3 in effect by reviewing response security headers and connection info (non-functional verification).", + "expectedResult": "Two purchases post successfully; include_rewards=false omits points_balance; include_rewards=true returns points_balance reflecting floor rules (Travel ×3, others ×1) with correct totals; owner-only access enforced with 403 for other accounts; masking is respected; invalid include_rewards value falls back to default behavior.", + "apiEndpoint": "POST /v2/accounts/{account_id}/transactions\nGET /v2/accounts/{account_id}/summary\nGET /v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "POST, GET, GET", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: for POST\nContent-Type: application/json", + "requestBody": "Transactions: transaction_amount, merchant_name, merchant_id, mcc_code (4722 or 5942), transaction_type 'PURCHASE', description", + "responseCodesExpected": "200,403", + "role": "Cardholder", + "stateBefore": "Account active with baseline points_balance known (possibly 0); sufficient available_credit", + "stateAfter": "Two purchases recorded; summary retrieved with and without rewards; no unauthorized access granted", + "calculationFormula": "Rewards: Travel floor(amount×3) + Others floor(amount×1) with floor rounding only", + "mccCode": "4722 Travel, 5942 Books", + "currency": "CAD", + "csrfTokenPresent": "true for transaction POST; false for GETs", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only; no full PAN in any responses", + "auditLogExpected": "Transaction audit entries for the two purchases; no credit_limit audit change", + "regulatoryRefs": "REQ-012 Rewards,REQ-013 Rounding,REQ-014 Data Masking,Owner-only enforcement", + "errorCodeExpected": "FORBIDDEN", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: points_balance is omitted unless include_rewards=true; ASSUMPTION: invalid boolean values default to false; ASSUMPTION: statement check may be deferred until statement generation." + }, + { + "type": "functional", + "title": "Credit application Step 2 idempotency and cross-application session token misuse", + "description": "Ensure Step 2 financial submission is idempotent for the same application/session, prevents duplicate soft pulls, and rejects session tokens used with a different application_id.", + "testId": "AEGIS-APP-IDEMP-022", + "testDescription": "Validates single-credit-pull behavior on repeated Step 2 calls, concurrent submissions, and 401 when X-App-Session is mismatched or expired.", + "prerequisites": "Authenticated user with verified email; no active application at test start; ability to simulate concurrent HTTP calls; test clock in UTC.", + "stepsToPerform": "1. Start Step 1: POST /v2/applications/start with valid personal and ID fields (full_legal_name, email matching user, phone +E.164, residential_address with ON and M5H 1J9, id_type DRIVERS_LICENSE); include X-CSRF-Token; capture application_id A1 and session_token S1.\n2. Submit Step 2 once: POST /v2/applications/A1/financials with X-App-Session: S1 and valid financials (EMPLOYED, employer_name 'Aegis QA', gross_annual_income 85000.00, monthly_rent 1200.00, existing_debt_payments 200.00, sin_consent true); expect 200 with status PENDING_REVIEW and fico_pull_id F1.\n3. Immediately resubmit Step 2 identically for A1 using the same S1; expect 200 and verify the same fico_pull_id F1 is returned (idempotent) and no new pull is created (ASSUMPTION: server reuses F1).\n4. Fire two concurrent Step 2 requests for A1 with S1 (simulate via parallel calls); expect one 200 with F1 and the other either 200 reusing F1 or a safe 200 with same status; ensure no duplicate soft pull entries are created (verify through response or logs if available).\n5. Start a second application: POST /v2/applications/start again with the same user after completing Step 3 for A1 later (ASSUMPTION: allowed when no active application); capture application_id A2 and session_token S2.\n6. Attempt Step 2 for A2 but mistakenly send X-App-Session: S1 (from A1) in header; expect 401 SESSION_EXPIRED or invalid session for mismatched token; confirm no financials saved for A2.\n7. Retry Step 2 for A2 with correct X-App-Session: S2 and valid financials; expect 200 PENDING_REVIEW with fico_pull_id F2; verify F2 ≠ F1.\n8. Proceed to Step 3 for A1: POST /v2/applications/A1/submit with valid card_product_id and e_signature; expect a valid decision (any of APPROVED/PENDING/DECLINED per bureau stub); ensure no additional credit pull occurs at Step 3.\n9. Attempt Step 2 for A1 again after Step 3 decision using S1; expect 401 SESSION_EXPIRED or 400 invalid state; verify the system prevents post-decision financial resubmission.\n10. Inspect all responses for masking and CSRF: ensure CSRF was required on all POSTS, SameSite=Strict cookies present, and no PAN or SSN beyond masked formats in any payloads.", + "expectedResult": "Initial Step 2 creates a single fico_pull_id; repeat and concurrent Step 2 calls reuse the same pull and do not create duplicates; using a session_token from a different application_id yields 401 SESSION_EXPIRED; post-decision Step 2 attempts are rejected; CSRF is enforced; no PII leakage.", + "apiEndpoint": "POST /v2/applications/start\nPOST /v2/applications/{application_id}/financials\nPOST /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nX-App-Session: \nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Step1: full_legal_name,email,phone_number,residential_address (street,city,province,postal_code),id_type,id_number\nStep2: employment_status,employer_name,gross_annual_income,other_income,monthly_rent,existing_debt_payments,sin_consent\nStep3: card_product_id,e_signature", + "responseCodesExpected": "201,200,400,401", + "role": "Applicant", + "stateBefore": "No active credit application for the user", + "stateAfter": "Two applications created sequentially (A1 completed decision, A2 pending review); no duplicate credit pulls; no state corruption", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE", + "piiMaskingExpected": "No PAN present; SSN masked if surfaced as ***-**-1234; masked PAN only where applicable", + "auditLogExpected": "Application events recorded; no credit_limit audit unless approved at Step 3", + "regulatoryRefs": "NFR-05 CSRF,NFR-06 Session Timeout", + "errorCodeExpected": "SESSION_EXPIRED,SIGNATURE_REQUIRED (if omitted), none for idempotent Step 2", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Idempotent Step 2 returns the same fico_pull_id; ASSUMPTION: Post-decision Step 2 is rejected; ASSUMPTION: All times UTC." + }, + { + "type": "functional", + "title": "CSRF token binding and invalid-token rejection across state-changing endpoints", + "description": "Validate that X-CSRF-Token is session-bound; stale or forged tokens are rejected across multiple endpoints even with valid JWT cookies.", + "testId": "AEGIS-CSRF-BIND-023", + "testDescription": "Covers CSRF invalid-token use after logout/login, per-endpoint enforcement, and confirmation that valid regenerated tokens permit the same actions.", + "prerequisites": "Verified user account; ability to capture and reuse old CSRF tokens; browser session capable of logging out/in; account_id and card_id owned by user; linked bank account.", + "stepsToPerform": "1. Log in via portal and load CSRF token T1 from bootstrap; confirm JWT cookies are HttpOnly/Secure and SameSite=Strict; ensure no tokens in localStorage.\n2. Perform a valid state-changing call with T1 to establish baseline: POST /v2/accounts/{account_id}/transactions for $1.00 PURCHASE (MCC 5942); expect 200 with transaction_id.\n3. Log out (ASSUMPTION: portal provides logout or cookie clear) and immediately log back in to obtain a fresh CSRF token T2; verify T2 differs from T1.\n4. Attempt POST /v2/accounts/{account_id}/payments using the stale token T1; expect 403 CSRF_MISSING or CSRF_INVALID (ASSUMPTION: CSRF_INVALID) and no payment created.\n5. Retry the same payments POST using the valid token T2; expect 200 accepted/queued with payment_id and new_balance_estimate.\n6. Attempt PATCH /v2/cards/{card_id}/status with status 'Frozen' using stale token T1 and a valid confirm_otp; expect 403 CSRF_INVALID and no status change.\n7. Retry the PATCH with valid token T2 and valid confirm_otp; expect 200 with new_status Frozen; then unfreeze similarly with T2 to return to Active.\n8. Attempt PUT /v2/cards/{card_id}/pin using stale T1, valid new_pin '1234', confirm_pin '1234', session_otp valid; expect 403 CSRF_INVALID and no PIN set.\n9. Retry the PIN PUT with T2 and valid OTP; expect 200 success.\n10. Attempt DELETE /v2/accounts/{id} using stale T1 (do not actually close in production data; use a test account or abort after error); expect 403 CSRF_INVALID and no closure; do not proceed with valid delete to preserve test account.\n11. Inspect all error responses for absence of PII and confirm masked PAN only where applicable; confirm SameSite=Strict on cookies throughout.", + "expectedResult": "All attempts with stale/invalid CSRF tokens are rejected with 403 and no state change; reattempts with the fresh token succeed; enforcement is consistent across POST, PATCH, PUT, DELETE; no PII leakage in errors.", + "apiEndpoint": "POST /v2/accounts/{account_id}/transactions\nPOST /v2/accounts/{account_id}/payments\nPATCH /v2/cards/{card_id}/status\nPUT /v2/cards/{card_id}/pin\nDELETE /v2/accounts/{id}", + "httpMethod": "POST, POST, PATCH, PUT, DELETE", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Transactions: transaction_amount=1.00, merchant_name, merchant_id, mcc_code=5942, transaction_type=PURCHASE\nPayments: payment_amount=5.00, payment_type=CUSTOM, bank_account_id\nCard status: status, reason, confirm_otp\nPIN: new_pin=1234, confirm_pin=1234, session_otp\nDELETE: none (path-only)", + "responseCodesExpected": "200,403", + "role": "Cardholder", + "stateBefore": "Active account and card; no freeze applied; no PIN recently set", + "stateAfter": "Card returned to Active; one small transaction and one payment created via valid CSRF; no unintended changes from invalid CSRF attempts", + "calculationFormula": "", + "mccCode": "5942 Books", + "currency": "CAD", + "csrfTokenPresent": "true (valid) and intentionally invalid for negatives", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only where referenced; no SSN in errors", + "auditLogExpected": "Freeze/unfreeze and PIN set events logged with user_id,session_id,ip_address,timestamp_utc", + "regulatoryRefs": "NFR-05 CSRF,PCI-DSS L1", + "errorCodeExpected": "CSRF_INVALID (ASSUMPTION), CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Server distinguishes invalid token as CSRF_INVALID; ASSUMPTION: Logout clears CSRF and session; ASSUMPTION: Test account not actually closed." + }, + { + "type": "functional", + "title": "Essential over-limit buffer lifecycle: multiple approvals, non-essential decline, and recovery after payment", + "description": "Validate 5% essential-service over-limit buffer consumption across sequential transactions, ensure non-essential declines while over buffer, and confirm payment restores available_credit.", + "testId": "AEGIS-TXN-BUFFER-LIFE-024", + "testDescription": "Exercises buffer usage exactly to 5% limit, tests subsequent non-essential denial, and verifies payment updates available_credit to allow new purchases.", + "prerequisites": "Active account with credit_limit and available_credit exactly CAD 1,000.00; card Active; valid CSRF token; essential MCCs include 5411 Grocery, 4900 Utilities; non-essential MCC 5942 Books.", + "stepsToPerform": "1. Verify available_credit is CAD 1,000.00 via GET /v2/accounts/{account_id}/summary; expect 200 and account_status Active.\n2. Submit an essential PURCHASE: POST /v2/accounts/{account_id}/transactions amount 600.00 MCC 5411; expect 200 approved with over_limit_flag false and available_credit ≈ 400.00.\n3. Submit a second essential PURCHASE: amount 450.00 MCC 4900; expect 200 approved with over_limit_flag true (buffer used) and available_credit reflecting -50.00 (within 5% buffer of 1,000.00); confirm item shows over_limit_flag true.\n4. Attempt a non-essential PURCHASE while in buffer: amount 5.00 MCC 5942; expect 402 INSUFFICIENT_FUNDS and no change to available_credit.\n5. Attempt another essential PURCHASE that would exceed buffer by $1 (e.g., 51.00 MCC 5411); expect 402 INSUFFICIENT_FUNDS and no change to available_credit.\n6. Make a payment to recover: POST /v2/accounts/{account_id}/payments payment_amount 60.00, payment_type CUSTOM, valid bank_account_id; expect 200 accepted with new_balance_estimate updated.\n7. Re-check summary: GET /v2/accounts/{account_id}/summary; expect available_credit >= 10.00 (buffer overdrawn -50 + 60 payment = +10).\n8. Retry non-essential PURCHASE: amount 5.00 MCC 5942; expect 200 approved now that available_credit is positive; verify available_credit decreases accordingly.\n9. Confirm CSRF enforcement: repeat a transaction POST without X-CSRF-Token; expect 403 CSRF_MISSING and no transaction created; then retry with token to succeed.\n10. Inspect all responses for masked PAN only and verify owner-only access by attempting the same POSTs as a different user (expect 403 FORBIDDEN and no leakage).", + "expectedResult": "Two essential purchases approve, with the second using the 5% buffer; non-essential while in buffer is declined; essential exceeding buffer declines; a payment restores available_credit and enables non-essential purchase; CSRF and owner-only enforcement hold; no PII leakage.", + "apiEndpoint": "GET /v2/accounts/{account_id}/summary\nPOST /v2/accounts/{account_id}/transactions\nPOST /v2/accounts/{account_id}/payments", + "httpMethod": "GET, POST, POST", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Transactions: transaction_amount, merchant_name, merchant_id, mcc_code (5411, 4900, 5942), transaction_type=PURCHASE, description\nPayments: payment_amount=60.00, payment_type=CUSTOM, bank_account_id", + "responseCodesExpected": "200,402,403", + "role": "Cardholder", + "stateBefore": "Account Active with available_credit 1,000.00; no holds", + "stateAfter": "Multiple transactions recorded; payment posted; available_credit positive and consistent; no invalid approvals", + "calculationFormula": "Buffer rule: approve essential up to available_credit × 1.05; non-essential requires available_credit ≥ amount", + "mccCode": "5411 Grocery (essential), 4900 Utilities (essential), 5942 Books (non-essential)", + "currency": "CAD", + "csrfTokenPresent": "true for valid posts; negative test omits", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only; no full PAN", + "auditLogExpected": "Transaction audit entries; no credit_limit audit changes", + "regulatoryRefs": "REQ-006 FX Fee (not used here), Owner-only enforcement,NFR-05 CSRF", + "errorCodeExpected": "INSUFFICIENT_FUNDS, CSRF_MISSING, FORBIDDEN", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Essential MCCs include 5411 and 4900; ASSUMPTION: Available_credit can reflect temporary negative within 5% buffer for essential approvals." + }, + { + "type": "functional", + "title": "Payments: MINIMUM and STATEMENT_BALANCE positive flows with precision enforcement", + "description": "Verify successful immediate MINIMUM and STATEMENT_BALANCE payments, enforce Decimal(10,2) precision, and confirm new_balance_estimate adjustments.", + "testId": "AEGIS-PAY-TYPES-025", + "testDescription": "Covers happy paths for MINIMUM and STATEMENT_BALANCE payments and rejects amounts with more than two decimal places or exceeding total_balance.", + "prerequisites": "Active account with an open statement and non-zero total_balance; known minimum_payment_due and statement balance; linked valid bank_account_id; valid JWT and X-CSRF-Token; test clock in UTC.", + "stepsToPerform": "1. Retrieve current statement via GET /v2/accounts/{account_id}/statements/{statement_id}?format=JSON; capture minimum_payment_due and statement_balance (ASSUMPTION: statement_balance available via statement fields or derived).\n2. Attempt POST /v2/accounts/{account_id}/payments with payment_amount 10.999 (three decimals), payment_type CUSTOM; expect 400 validation error due to Decimal(10,2) scale and no payment created.\n3. Attempt POST with payment_amount greater than total_balance by $0.01, payment_type CUSTOM; expect server to reject (ASSUMPTION: 400 or 422) and no payment created.\n4. Submit MINIMUM payment: POST /v2/accounts/{account_id}/payments with payment_type MINIMUM (omit payment_amount), bank_account_id valid; expect 200 accepted with payment_id and new_balance_estimate decreased by minimum_payment_due.\n5. Confirm via GET /v2/accounts/{account_id}/summary that current_balance reflects the queued payment effect in new_balance_estimate field from response if not posted yet; verify no inconsistency.\n6. Submit STATEMENT_BALANCE payment: POST /v2/accounts/{account_id}/payments with payment_type STATEMENT_BALANCE (omit payment_amount), bank_account_id valid; expect 200 accepted with new_balance_estimate reduced by the statement balance amount.\n7. Attempt another payment immediately with the same idempotent client token if such exists (ASSUMPTION: none); verify that duplicate processing does not occur (manual visual check of queued payments count or response indicates accepted once).\n8. Attempt a repeat POST without X-CSRF-Token to ensure enforcement; expect 403 CSRF_MISSING with no additional payment queued.\n9. Inspect all responses for correct masking: no PAN present; confirm SameSite=Strict on cookies and TLS 1.3 in effect (non-functional).", + "expectedResult": "Amounts with >2 decimals are rejected; overpayments above total_balance are rejected; MINIMUM and STATEMENT_BALANCE payments are accepted and return correct new_balance_estimate deltas; CSRF is enforced; no PII leakage.", + "apiEndpoint": "GET /v2/accounts/{account_id}/statements/{statement_id}\nPOST /v2/accounts/{account_id}/payments\nGET /v2/accounts/{account_id}/summary", + "httpMethod": "GET, POST, GET", + "requestHeaders": "Authorization: Bearer \nX-CSRF-Token: \nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Payments: payment_type {MINIMUM or STATEMENT_BALANCE} (omit payment_amount), bank_account_id; Negative tests: payment_amount with 3 decimals or > total_balance", + "responseCodesExpected": "200,400,403,422", + "role": "Cardholder", + "stateBefore": "Account with outstanding statement and minimum due; linked bank account set up", + "stateAfter": "Two payments queued (MINIMUM and STATEMENT_BALANCE) and visible; invalid attempts rejected without side effects", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for valid posts; false for deliberate negative", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "No PAN in payment responses; masked identifiers only if referenced", + "auditLogExpected": "Standard payment submission entries; no credit_limit audit changes", + "regulatoryRefs": "NFR-05 CSRF,PCI-DSS L1", + "errorCodeExpected": "BELOW_MINIMUM (if mis-specified), CSRF_MISSING, INVALID_BANK_ACCOUNT (if bank invalid)", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Overpayment above total_balance returns 400/422; ASSUMPTION: Statement balance available via statement or derived; ASSUMPTION: All times UTC." + }, + { + "type": "functional", + "title": "Global PII leakage sweep on error payloads and UI across modules", + "description": "Produce assorted 400/401/403/404/422 errors across endpoints and verify that responses and UI contain no unmasked PAN/SSN and adhere to masking rules.", + "testId": "AEGIS-PII-SWEEP-026", + "testDescription": "Compliance-focused test to ensure PCI-DSS masking (REQ-014) in all error pathways and absence of PII in browser logs and DOM.", + "prerequisites": "Browser devtools available; user account with owned account_id and card_id; ability to act as non-owner for cross-account tests; valid and invalid payloads prepared; test clock UTC.", + "stepsToPerform": "1. Registration error: POST /v2/auth/register with invalid email 'user@@example..com' and underage DOB; expect 400 with field errors; inspect body for absence of PAN and SSN (only ***-**-1234 if referenced); check console logs for no PII.\n2. Login error: POST /v2/auth/login with wrong password; expect 401 INVALID_CREDENTIALS; verify response and UI contain no PII and no token exposure; cookies remain secure.\n3. Transactions validation error: POST /v2/accounts/{account_id}/transactions with transaction_amount 0.00; expect 422 INVALID_AMOUNT; verify no PAN/SSN in payload or UI error banner.\n4. Owner-only error: GET /v2/accounts/{account_id_other}/summary as current user; expect 403 FORBIDDEN; confirm response leaks no account fields and no masked PAN either.\n5. Statement not found: GET /v2/accounts/{account_id}/statements/{random_id}; expect 404 NOT_FOUND; ensure no sensitive data present.\n6. Payments invalid bank: POST /v2/accounts/{account_id}/payments with invalid bank_account_id; expect 422 INVALID_BANK_ACCOUNT; verify no PAN and that any identifiers are masked.\n7. Card status OTP failure: PATCH /v2/cards/{card_id}/status with bad confirm_otp; expect 401 OTP_FAILED with attempts_remaining; confirm no PAN/SSN in body.\n8. PIN format error: PUT /v2/cards/{card_id}/pin with new_pin '12A4' (non-numeric); expect 400 PIN_FORMAT; check response for masking and UI for no PII.\n9. CSRF missing: POST /v2/applications/{application_id}/submit without X-CSRF-Token; expect 403 CSRF_MISSING/CSRF_INVALID; inspect that no PII is returned.\n10. Browser/Network log audit: Inspect network tab and console for each above call to ensure no full PAN appears in error payloads, headers, or DOM; verify masking policy of PAN **** **** **** 1234 if present, and SameSite=Strict cookies are used.", + "expectedResult": "All error responses across modules contain no full PAN or SSN; masking policy holds; UI and logs show no PII leakage; security headers and cookie flags remain intact.", + "apiEndpoint": "POST /v2/auth/register\nPOST /v2/auth/login\nPOST /v2/accounts/{account_id}/transactions\nGET /v2/accounts/{account_id_other}/summary\nGET /v2/accounts/{account_id}/statements/{statement_id}\nPOST /v2/accounts/{account_id}/payments\nPATCH /v2/cards/{card_id}/status\nPUT /v2/cards/{card_id}/pin\nPOST /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST, GET, GET, POST, PATCH, PUT, POST", + "requestHeaders": "Authorization: Bearer for protected endpoints\nX-CSRF-Token: \nContent-Type: application/json\nCookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Register invalid: first_name,last_name,email invalid,password,date_of_birth under 18,phone,ssn_last4,agree_terms\nLogin invalid: email,password (wrong)\nTransactions invalid: transaction_amount=0.00, merchant fields\nPayments invalid: payment_amount, payment_type, invalid bank_account_id\nCard status: status, confirm_otp invalid\nPIN: new_pin '12A4', confirm_pin '12A4', session_otp\nSubmit: card_product_id,e_signature (with missing CSRF)", + "responseCodesExpected": "400,401,403,404,422", + "role": "Cardholder,Prospective Applicant (for registration)", + "stateBefore": "Active account and card for user; ability to attempt cross-account access for negative tests", + "stateAfter": "No state changes except standard OTP attempt decrement; no sensitive data exposure", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "Mixed: present for most, deliberately absent for CSRF negative", + "authTokenType": "JWT in HttpOnly, Secure cookies; public endpoints without JWT for register", + "piiMaskingExpected": "PAN **** **** **** 1234 only if ever referenced; SSN masked as ***-**-1234; no raw PAN in DOM or API", + "auditLogExpected": "Security events logged by WAF; no credit_limit audit changes", + "regulatoryRefs": "REQ-014 Data Masking,PCI-DSS L1,NFR-05 CSRF", + "errorCodeExpected": "INVALID_CREDENTIALS, FORBIDDEN, NOT_FOUND, INVALID_AMOUNT, INVALID_BANK_ACCOUNT, OTP_FAILED, PIN_FORMAT, CSRF_MISSING", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Cross-account summary returns 403 without metadata; ASSUMPTION: CSRF error code string may be CSRF_MISSING or CSRF_INVALID." + }, + { + "type": "functional", + "title": "Credit application draft auto-save and resume with PII sanitization and timeout recovery", + "description": "Validate 60-second draft auto-save for Step 1, ensure localStorage draft is sanitized (no SSN/ID values), supports browser restart resume, clears on successful submit, and handles inactivity warning without data loss.", + "testId": "AEGIS-APP-AUTOSAVE-027", + "testDescription": "Covers UI auto-save to localStorage for Step 1, resume flow, CSRF use on submit, draft clearing semantics, and inactivity warning handling per NFR-06.", + "prerequisites": "Authenticated user with verified email and valid JWT in HttpOnly, Secure, SameSite=Strict cookies; CSRF token loaded by portal; test clock in UTC; no active application.", + "stepsToPerform": "1. Navigate to portal Credit Application Step 1 and verify CSRF token is present in bootstrap; confirm no tokens stored in localStorage/sessionStorage.\n2. Begin typing valid Step 1 fields (full_legal_name 'Jordan QA', email equals logged-in user, phone '+14165550123', address street/city '100 King St W'/'Toronto', province 'ON', postal_code 'M5H 1J9', id_type 'DRIVERS_LICENSE', id_number 'JRDN12345') but do NOT submit.\n3. Wait at least 60 seconds to allow auto-save timer to run; open browser devtools and inspect localStorage for a draft key (e.g., 'aegis.application.draft').\n4. Verify the draft JSON contains only Step 1 data and is sanitized: no ssn_last4 present and id_number and phone are present only if required; ensure no PAN or sensitive tokens are stored; confirm lastSaved timestamp updated within last minute.\n5. Simulate browser restart (close tab, reopen portal) and navigate back to Credit Application; click 'Resume draft' when prompted; confirm all previously entered fields are re-populated exactly.\n6. Trigger inactivity behavior: remain idle for 13 minutes; verify a 2-minute warning modal appears; click 'Stay signed in' within the 2-minute window and confirm the modal closes and form data remains intact.\n7. Proceed to submit Step 1 by clicking Continue; ensure POST /v2/applications/start is sent with X-CSRF-Token header and valid body; capture 201 response with application_id and session_token.\n8. Immediately recheck localStorage for the draft key; verify it is removed or marked as cleared (e.g., status 'consumed'), preventing stale resume prompts on refresh.\n9. Attempt to reload the page again; verify the form no longer offers the resume option and loads empty fields, confirming draft was cleared on successful submit.\n10. Inspect network responses and UI for PCI masking compliance: confirm no PAN present and PII is limited; verify SameSite=Strict cookies persist throughout the flow.", + "expectedResult": "Draft auto-saves at 60s intervals with sanitized content (no SSN/PAN), survives browser restart and accurately repopulates Step 1, the inactivity warning allows extending the session without data loss, Step 1 submits with CSRF to return 201 and application_id/session_token, and the draft is cleared thereafter; no unmasked PII is exposed.", + "apiEndpoint": "POST /v2/applications/start", + "httpMethod": "POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "requestBody": "full_legal_name, email, phone_number, residential_address {street, city, province, postal_code}, id_type, id_number", + "responseCodesExpected": "201,400,409", + "role": "Applicant", + "stateBefore": "No active application; no draft present in localStorage", + "stateAfter": "Draft cleared and new application created with session_token; no residual sensitive data in localStorage", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true on submit; auto-save is client-side only", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE", + "piiMaskingExpected": "No PAN; no SSN in drafts; UI and network show masked identifiers only", + "auditLogExpected": "Application start event logged; no credit_limit audit until approval", + "regulatoryRefs": "NFR-06 Session Timeout, NFR-05 CSRF, PCI-DSS L1", + "errorCodeExpected": "DUPLICATE_APPLICATION (if user had active app), field validation errors", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Draft key name follows 'aegis.application.draft'; ASSUMPTION: Draft clears on successful 201; ASSUMPTION: Times and inactivity timer are in UTC." + }, + { + "type": "functional", + "title": "Transactions maximum amount and FX precision boundaries with Decimal(8,6) exchange_rate", + "description": "Validate Decimal(10,2) transaction_amount maximum, reject overflow, accept boundary when credit allows, enforce exchange_rate precision scale, and compute FX fee and Total_CAD rounding precisely.", + "testId": "AEGIS-TXN-MAXFX-028", + "testDescription": "Covers high-value CAD and FX transactions at boundary limits, exchange_rate scale validation, and accurate REQ-006 fee math with rounding to two decimals.", + "prerequisites": "Account in Active status owned by user; available_credit at least CAD 10,000,000.00 (test env); valid CSRF token; TLS 1.3; MCCs available; test supports EUR currency.", + "stepsToPerform": "1. Attempt a CAD PURCHASE with transaction_amount 10000000.00 (one cent above Decimal(10,2) max 9,999,999.99) using MCC 5942, merchant_name 'Max Test', merchant_id 'MAXTXN01'; expect server-side validation error for amount exceeding maximum and no change to available_credit.\n2. Retry with transaction_amount exactly 9999999.99 CAD, same merchant/MCC, including X-CSRF-Token; expect 200 Approved with transaction_id, available_credit decreased appropriately, and auth_code present (requires sufficient credit as per prerequisites).\n3. Verify response carries no foreign_fee_amount for CAD default and includes masked PAN only if referenced (**** **** **** 1234).\n4. Attempt an FX PURCHASE in EUR with transaction_amount 1234.56, currency_code 'EUR', but exchange_rate 1.2345678 (7 decimal places); expect 400 validation error due to Decimal(8,6) precision breach for exchange_rate.\n5. Retry FX with exchange_rate 1.234567 (valid 6 decimal places), MCC 4722 (Travel), merchant_name 'Euro Travel', merchant_id 'EURTRV01'; include CSRF; expect 200 Approved with itemized foreign_fee_amount and Total_CAD.\n6. Manually compute Total_CAD per REQ-006: ((1234.56 × 1.234567) × 1.03); round final Total_CAD to two decimals; compute foreign_fee_amount = (1234.56 × 1.234567) × 0.03; round to two decimals; verify response matches both values to two decimals.\n7. Confirm that available_credit decreased by Total_CAD (rounded) and that precision is consistent with Decimal(10,2) response fields.\n8. Attempt an FX PURCHASE with exchange_rate 0.000000 (zero); expect 400 validation error for invalid exchange_rate > 0 requirement (server-side rule inferred from business logic).\n9. Re-run a valid CAD PURCHASE at minimal positive boundary: transaction_amount 0.01; expect 200 or 422 depending on > 0 enforcement (spec requires > 0.00), validate server accepts 0.01 as > 0 and approves if credit available.\n10. Inspect all responses for CSRF enforcement (state-changing POSTs must include X-CSRF-Token) and ensure no full PAN appears in payloads.", + "expectedResult": "Amounts above 9,999,999.99 are rejected; 9,999,999.99 CAD approves when credit allows; FX exchange_rate with >6 decimal places is rejected; valid 6-decimal exchange_rate computes Total_CAD and foreign_fee_amount per REQ-006 with correct two-decimal rounding; zero exchange_rate is rejected; minimal positive CAD 0.01 is accepted; all POSTs enforce CSRF and payloads mask PAN.", + "apiEndpoint": "POST /v2/accounts/{account_id}/transactions", + "httpMethod": "POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: , Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "requestBody": "transaction_amount, merchant_name, merchant_id, mcc_code (5942 or 4722), currency_code (CAD/EUR), exchange_rate (if FX), transaction_type 'PURCHASE', description", + "responseCodesExpected": "200,400,422", + "role": "Cardholder", + "stateBefore": "Account Active with available_credit ≥ 10,000,000.00; card Active", + "stateAfter": "High-value CAD and valid FX purchases posted; invalid attempts rejected with no balance change", + "calculationFormula": "REQ-006: Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03; round outputs to two decimals", + "mccCode": "5942 Books (non-Travel), 4722 Travel", + "currency": "CAD, EUR", + "csrfTokenPresent": "true for all valid POSTs; absent for none in this test", + "authTokenType": "JWT in HttpOnly, Secure cookies", + "piiMaskingExpected": "PAN **** **** **** 1234 only if referenced; otherwise no PAN fields", + "auditLogExpected": "Transaction audit entries created; no credit_limit audit changes", + "regulatoryRefs": "REQ-006 FX Fee, NFR-05 CSRF, PCI-DSS L1", + "errorCodeExpected": "INVALID_AMOUNT, validation error for exchange_rate precision, CSRF_MISSING (if header omitted)", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Server enforces Decimal(10,2) for transaction_amount and Decimal(8,6) for exchange_rate; ASSUMPTION: exchange_rate must be > 0; ASSUMPTION: Available credit suffices for boundary CAD approval." + }, + { + "type": "functional", + "title": "WebSocket unauthorized handshake, subscribe/unsubscribe lifecycle, and secure reconnect", + "description": "Ensure WS connection is rejected without valid JWT, successful after login, subscription controls work (subscribe/unsubscribe), and no messages arrive while unsubscribed.", + "testId": "AEGIS-WS-UNAUTH-029", + "testDescription": "Validates WS auth failure handling, proper subscription acks, unsubscribe behavior, and re-subscribe delivery without duplicates; confirms no token leakage to localStorage.", + "prerequisites": "User account exists; ability to publish test events to realtime stream; portal reachable; browser with WebSocket support; JWT available post-login.", + "stepsToPerform": "1. Attempt to connect to wss://realtime.aegiscard.com/v2/stream without any Authorization token or cookies; expect server to reject connection (e.g., HTTP 401 during WS upgrade or app-level 'auth_failed').\n2. Attempt to connect with an intentionally invalid/expired JWT (ASSUMPTION: via Sec-WebSocket-Protocol or Authorization header); expect immediate rejection and no subscription allowed.\n3. Log in via POST /v2/auth/login with correct credentials and MFA; verify JWT cookies are HttpOnly, Secure, SameSite=Strict and no tokens stored in localStorage/sessionStorage.\n4. Establish WS connection using valid JWT auth; expect 'connected' acknowledgement message.\n5. Send a subscribe frame for topic 'account:{account_id}'; expect 'subscribed' confirmation including topic name and timestamp.\n6. Publish a test event to this account with message_id 'MSG-1001', type 'transaction.authorized', amount 25.00, mcc 5942, masked_pan '**** **** **** 1234'; verify one UI feed entry received and rendered correctly.\n7. Issue an 'unsubscribe' frame for 'account:{account_id}'; expect 'unsubscribed' confirmation; publish another event message_id 'MSG-1002'; verify no new UI entries appear while unsubscribed.\n8. Re-subscribe to 'account:{account_id}'; expect 'subscribed' ack; publish 'MSG-1002' again and a new 'MSG-1003'; verify exactly one entry for MSG-1002 (no duplicates due to dedupe on message_id) and one for MSG-1003.\n9. Inspect WS messages to ensure all include masked_pan only and no SSN/PAN appears; verify the browser still stores no tokens in localStorage.\n10. Gracefully close the WS and reconnect to confirm auth flow repeats successfully without stale subscriptions; confirm no events delivered until re-subscribe completes.", + "expectedResult": "WS handshake is rejected without or with invalid JWT; after login, connection succeeds; subscribe delivers events; unsubscribe halts delivery; re-subscribe resumes delivery without duplicates; all payloads show masked PAN only; no tokens appear in localStorage.", + "apiEndpoint": "POST /v2/auth/login\nwss://realtime.aegiscard.com/v2/stream", + "httpMethod": "POST, WS", + "requestHeaders": "Authorization: Bearer for WS (ASSUMPTION: header or subprotocol), Cookie: HttpOnly Secure SameSite=Strict for login, Content-Type: application/json", + "requestBody": "Login: email,password,mfa_code; WS subscribe frame: topic 'account:{account_id}'", + "responseCodesExpected": "200 for login, 101 for WS upgrade, 401 for unauthorized WS", + "role": "Cardholder", + "stateBefore": "No active WS connection; user not logged in", + "stateAfter": "Active WS session with managed subscriptions; no unauthorized messages delivered", + "calculationFormula": "", + "mccCode": "5942 Books for sample event", + "currency": "CAD", + "csrfTokenPresent": "false for WS (not CSRF-protected), true for login form if applicable", + "authTokenType": "JWT in HttpOnly, Secure cookies; WS auth via header/subprotocol", + "piiMaskingExpected": "masked_pan **** **** **** 1234 only; no full PAN or SSN", + "auditLogExpected": "Connection/subscription events recorded for monitoring; no credit_limit audits", + "regulatoryRefs": "PCI-DSS L1", + "errorCodeExpected": "auth_failed or 401 on WS handshake; none for valid subscribe", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: WS auth uses Authorization header or subprotocol; ASSUMPTION: unsubscribe is supported in-app protocol; ASSUMPTION: client dedupes by message_id." + }, + { + "type": "functional", + "title": "Notifications webhook message length validation and channel-scoped idempotency across same account", + "description": "Validate message_body length ≤ 500 chars, sanitize basic HTML, enforce idempotency_key uniqueness per account across channels, and ensure no PII in delivered payloads.", + "testId": "AEGIS-NOTIF-LIMITS-030", + "testDescription": "Tests 400 on message_body > 500, delivery of allowed HTML content sanitized in UI, and that reusing an idempotency_key with a different channel for the same account returns 409.", + "prerequisites": "Service auth token to call internal webhook; account_id owned by test user for UI verification; in-app notification center available; email/PUSH/SMS stubs accessible.", + "stepsToPerform": "1. Prepare a message_body string of 501 characters; POST /v2/notifications/webhook with account_id A, alert_type 'OVER_LIMIT', channel 'IN_APP', severity 'WARNING', idempotency_key UUID K-OVERLIM-1; expect 400 INVALID_ALERT_TYPE or length validation error (message too long) and no delivery.\n2. Reduce message_body to 500 characters (exact boundary) with simple HTML tags (e.g., 'Alert Statement ready'); POST with same other fields and a new idempotency_key K-OVERLIM-2; expect 200 success with notification_id and delivered_at.\n3. Open the portal notifications center and verify that the message renders safely (e.g., '' interpreted or sanitized per UI policy) without script execution; confirm no PII such as PAN/SSN appears.\n4. Replay the exact same payload with idempotency_key K-OVERLIM-2; expect 409 DUPLICATE_NOTIFICATION and no duplicate UI entry.\n5. Attempt to send the same logical alert but with a different channel for the same account: change channel to 'EMAIL' while keeping idempotency_key K-OVERLIM-2; expect 409 DUPLICATE_NOTIFICATION (key uniqueness per account across channels).\n6. Send a distinct alert with a fresh idempotency_key K-FRAUD-1: alert_type 'FRAUD_FLAG', channel 'PUSH', severity 'CRITICAL', message_body 'Potential fraud detected'; expect 200 and one delivery in the PUSH stub.\n7. Attempt to call the webhook without the required service authorization header; expect 401/403 FORBIDDEN and no processing of the payload.\n8. Inspect all webhook responses and UI notifications to ensure masked identifiers only and that no PAN/SSN is present in any path.\n9. Confirm channel entries (IN_APP, EMAIL, PUSH) show correct channel labels/timestamps and no duplicates when reusing the same idempotency_key.\n10. Verify TLS 1.3 and response headers indicate secure transport (non-functional check).", + "expectedResult": "Overlong message_body is rejected with 400; 500-char message is accepted and displays safely in UI without PII; duplicates with same idempotency_key return 409 even when channel differs for the same account; authorized calls succeed and unauthorized fail; exactly one delivery occurs per unique key/account.", + "apiEndpoint": "POST /v2/notifications/webhook", + "httpMethod": "POST", + "requestHeaders": "Authorization: , Content-Type: application/json", + "requestBody": "account_id, alert_type, channel, message_body (≤ 500 chars), severity, idempotency_key", + "responseCodesExpected": "200,400,409,401,403", + "role": "System Integration (Internal Service)", + "stateBefore": "No notifications for idempotency keys used; stubs are empty", + "stateAfter": "One IN_APP and one PUSH notification delivered; duplicates rejected by 409", + "calculationFormula": "", + "mccCode": "", + "currency": "", + "csrfTokenPresent": "false (internal endpoint not CSRF-protected)", + "authTokenType": "Internal service auth (not end-user JWT)", + "piiMaskingExpected": "No PAN or SSN in any payloads or UI; masked identifiers only if included", + "auditLogExpected": "Webhook delivery logs include idempotency_key and channel; no card data exposure", + "regulatoryRefs": "PCI-DSS L1", + "errorCodeExpected": "INVALID_ALERT_TYPE (for invalid channel/type), DUPLICATE_NOTIFICATION, FORBIDDEN", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Idempotency key uniqueness is per account across all channels; ASSUMPTION: UI sanitizes basic HTML and blocks scripts." + }, + { + "type": "functional", + "title": "Session warning extend flow during payment form and mid-flow token refresh with CSRF continuity", + "description": "Validate the 2-minute warning extend action keeps session alive during a payment form, submission succeeds with CSRF, and a mid-flow token refresh does not break CSRF validation.", + "testId": "AEGIS-SESSION-EXTEND-031", + "testDescription": "Ensures NFR-06 warning modal 'Stay signed in' extends the session, form data persists, and CSRF continues to work even if a token refresh occurs just before submit.", + "prerequisites": "Active account with outstanding balance and linked bank_account_id; user logged in with valid JWT; CSRF token loaded; test clock UTC; access_token default expiry ~15 minutes; refresh token valid.", + "stepsToPerform": "1. Navigate to Make a Payment form and confirm CSRF token is present; verify cookies are HttpOnly, Secure, SameSite=Strict and no tokens exist in localStorage.\n2. Enter a valid immediate payment: payment_type 'CUSTOM', payment_amount 5.00, select valid bank_account_id but DO NOT submit yet.\n3. Idle on the form for 13 minutes; confirm a 2-minute warning modal appears indicating imminent logout.\n4. Click 'Stay signed in' within the modal; verify the modal disappears and the form fields retain their values without reloading the page.\n5. In a separate devtools console, call POST /v2/auth/token/refresh to simulate automatic background refresh (portal may do this); verify 200 and cookies rotate; confirm refresh token single-use enforcement by attempting to reuse the old refresh (should yield 401 TOKEN_INVALID) without affecting the current tab.\n6. Attempt to submit the payment immediately after refresh without reloading CSRF; ensure POST /v2/accounts/{account_id}/payments includes X-CSRF-Token and expect 200 accepted with payment_id and new_balance_estimate; confirm CSRF continuity across token rotation.\n7. Verify the response reflects the correct scheduled_date (omitted for immediate) and new_balance_estimate decreases appropriately.\n8. Attempt a second identical payment submission by re-clicking submit quickly; expect either a UI-level prevention or server-side duplicate prevention (no duplicate payment created; ASSUMPTION: server returns 200 once and ignores immediate repeat or a specific duplicate error if implemented).\n9. Inspect network logs to ensure no PAN appears in the payment response and no PII in console logs; confirm SameSite=Strict remains set on cookies.\n10. Remain idle for an additional 15 minutes without interaction; verify auto-logout occurs and subsequent API call to payments returns 401 requiring re-authentication.", + "expectedResult": "The 'Stay signed in' action extends the session without losing form data; a mid-flow token refresh rotates tokens while CSRF remains valid; payment submission returns 200 with correct new_balance_estimate; duplicate submission is not processed twice; after extended inactivity, auto-logout occurs and protected endpoints return 401.", + "apiEndpoint": "POST /v2/auth/token/refresh\nPOST /v2/accounts/{account_id}/payments", + "httpMethod": "POST, POST", + "requestHeaders": "Authorization: Bearer , X-CSRF-Token: for payments, Content-Type: application/json, Cookie: HttpOnly Secure SameSite=Strict", + "requestBody": "Payments: payment_amount=5.00, payment_type=CUSTOM, bank_account_id, scheduled_date omitted", + "responseCodesExpected": "200,401", + "role": "Cardholder", + "stateBefore": "User logged in, payment form loaded, access token near expiry, CSRF loaded", + "stateAfter": "One payment accepted; session maintained then later expired by inactivity; no duplicate payments created", + "calculationFormula": "", + "mccCode": "", + "currency": "CAD", + "csrfTokenPresent": "true for payment POST; refresh endpoint relies on cookies", + "authTokenType": "JWT in HttpOnly, Secure cookies with PKCE; refresh rotation enforced", + "piiMaskingExpected": "No PAN in payment responses; masked identifiers only if any appear", + "auditLogExpected": "Payment submission logged; no credit_limit audits", + "regulatoryRefs": "NFR-06 Session Timeout, NFR-05 CSRF, PCI-DSS L1", + "errorCodeExpected": "TOKEN_INVALID (old refresh reuse), potential client-side duplicate prevention", + "paginationParams": "", + "featureFlag": "", + "assumptions": "ASSUMPTION: Portal performs background token refresh; ASSUMPTION: CSRF token remains valid post-refresh; ASSUMPTION: Duplicate submission is prevented by UI or server idempotency." + } +] \ No newline at end of file diff --git a/functional_tests/functional-test-generation/functional-test-generation.xlsx b/functional_tests/functional-test-generation/functional-test-generation.xlsx new file mode 100644 index 0000000..37edaf3 Binary files /dev/null and b/functional_tests/functional-test-generation/functional-test-generation.xlsx differ