diff --git a/functional_tests/README.md b/functional_tests/README.md index 1c972b2..485e3e5 100644 --- a/functional_tests/README.md +++ b/functional_tests/README.md @@ -65,3 +65,20 @@ --- +**Execution Date:** 4/28/2026, 7:19:28 AM + +**Test Unique Identifier:** "functional-test-aegis" + +**Input(s):** + 1. Aegis_WebCC_SRS.pdf + Path: /var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/Aegis_WebCC_SRS.pdf + +**Test Output Folder:** + 1. [functional-test-aegis.json](functional-test-aegis/functional-test-aegis.json) + 2. [functional-test-aegis.feature](functional-test-aegis/functional-test-aegis.feature) + 3. [functional-test-aegis.csv](functional-test-aegis/functional-test-aegis.csv) + 4. [functional-test-aegis.xlsx](functional-test-aegis/functional-test-aegis.xlsx) + 5. [functional-test-aegis.docx](functional-test-aegis/functional-test-aegis.docx) + +--- + diff --git a/functional_tests/functional-test-aegis/.roost/roost_metadata.json b/functional_tests/functional-test-aegis/.roost/roost_metadata.json new file mode 100644 index 0000000..1c6a258 --- /dev/null +++ b/functional_tests/functional-test-aegis/.roost/roost_metadata.json @@ -0,0 +1,24 @@ +{ + "project": { + "name": "functional-test-aegis", + "created_at": "2026-04-28T07:19:28.118Z", + "updated_at": "2026-04-28T07:19:28.118Z" + }, + "files": { + "input_files": [ + { + "fileName": "functional-test-aegis.txt", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/functional_tests/functional-test-aegis/functional-test-aegis.txt", + "fileSha": "cf83e1357e" + }, + { + "fileName": "Aegis_WebCC_SRS.pdf", + "fileURI": "/var/tmp/Roost/RoostGPT/functional-test-aegis/d6bf0345-379c-48af-b81d-1f8f88aaab5e/functional_tests/functional-test-aegis/Aegis_WebCC_SRS.pdf", + "fileSha": "dcebdb1a12" + } + ] + }, + "api_files": { + "input_files": [] + } +} \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.csv b/functional_tests/functional-test-aegis/functional-test-aegis.csv new file mode 100644 index 0000000..0a3f51c --- /dev/null +++ b/functional_tests/functional-test-aegis/functional-test-aegis.csv @@ -0,0 +1,29 @@ +End-to-End Applicant Journey with Security Controls (Register -> Verify -> Login MFA -> Application -> APPROVED -> Token Rotation -> CSRF Enforcement -> PIN -> Summary -> Session Timeout) +Registration Validations and Case-Insensitive Email Uniqueness +Authentication Lockout, Rate Limit, Remember Me TTL, Logout Invalidation +Refresh Token Rotation Concurrency Across Tabs and CSRF Session Binding +Access Control and CSRF Enforcement Matrix with SameSite=Strict +Foreign Currency Purchase, Rewards, Pagination, CSRF, Rate Limit, Statements and Grace +Transaction Field Validation and FX Precision +Valid Small-Value FX with Rounding and Listing Page Validation +Transactions Date/Time Boundaries, UTC/DST, per_page Max, Stable Pagination +Rewards Accrual Boundary and MCC Classification with Floor Rounding +Transaction Rate-Limit Recovery via MFA +WebSocket Live Feed Resilience and Authorization +Card Controls Freeze/Unfreeze with OTP and CSRF, Transactions Blocked While Frozen +Report Card STOLEN with Delivery Address Override, OTP, Audit, Irreversibility +Payments, Late Fee, Interest/Grace, Scheduling, CSRF, Rescind Window +Payments CUSTOM Boundary and CSRF +Statement Calculations Boundary: Interest Rounding, Grace, Late Fee UTC Boundary, Not Found +Application Step Order Enforcement, Session Token Validation, PENDING and DECLINED +Application Step 2 Credit Pull: sin_consent Enforcement, 503 Retry Idempotency, Session Expiry +Application Step 1 Validation, Autosave Privacy and Cross-User Isolation +Notifications Webhook Validation, PII Masking, Severity Rendering +Transaction Currency and Field Validation with Success at Precision Boundaries +CSRF Regeneration After Refresh Rotation +Phone OTP Verification Flow with Expiry, Resend Throttle, Attempts Remaining +Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF +Audit Trail for Credit Limit Changes and Access Control +Right to Rescind Security Sweep Post-Delete +Session Timeout Warning - Stay Signed In Flow and Auto-Logout +PAN Masking and Iframe Tokenization Compliance Across Portal Surfaces \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.docx b/functional_tests/functional-test-aegis/functional-test-aegis.docx new file mode 100644 index 0000000..ae139d3 Binary files /dev/null and b/functional_tests/functional-test-aegis/functional-test-aegis.docx differ diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.feature b/functional_tests/functional-test-aegis/functional-test-aegis.feature new file mode 100644 index 0000000..56767f1 --- /dev/null +++ b/functional_tests/functional-test-aegis/functional-test-aegis.feature @@ -0,0 +1,931 @@ +Feature: Aegis Card Portal - Security, Authentication, Applications, Transactions, Billing, and Compliance + + # Background common to most API tests + Background: + Given the API base URL is 'https://api.aegiscard.com/v2' + And the portal URL is 'https://portal.aegiscard.com' + And the realtime WebSocket URL is 'wss://realtime.aegiscard.com/v2/stream' + And the client enforces TLS 1.3 with HSTS and no mixed content + And the default request headers include 'Content-Type: application/json' + And no tokens are stored in localStorage or sessionStorage + + # API Tests + + @api @e2e @security + Scenario: End-to-End Applicant Journey with Security Controls (Register -> Verify -> Login MFA -> Application -> APPROVED -> Token Rotation -> CSRF Enforcement -> PIN -> Summary -> Session Timeout) + Given there is no existing account for 'user@example.com' and MFA seed is provisioned + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"Jane", + "last_name":"Public", + "email":"user@example.com", + "password":"Str0ng-P@ssword!2026", + "date_of_birth":"", + "phone_number":"+14165550123", + "ssn_last4":"1234", + "agree_terms":true + } + """ + Then the response status should be 201 + And the response JSON should contain fields 'user_id' and 'verification_token' + + When I send a POST request to '/auth/verify' with JSON payload + """ + { "verification_token": "" } + """ + Then the response status should be 200 + + When I send a POST request to '/auth/login' with JSON payload + """ + { + "email":"user@example.com", + "password":"Str0ng-P@ssword!2026", + "device_id":"550e8400-e29b-41d4-a716-446655440000", + "remember_me":true, + "mfa_code":"" + } + """ + Then the response status should be 200 + And HttpOnly, Secure, SameSite=Strict cookies for access_token and refresh_token should be set + And no tokens should exist in localStorage or be accessible to JavaScript + + When I send a GET request to '/auth/csrf' + Then the response status should be 200 + And I save the 'X-CSRF-Token' value for subsequent requests + + When I send a POST request to '/applications/start' with headers 'X-CSRF-Token' and JSON payload + """ + { + "full_legal_name":"Jane Q Public", + "email":"user@example.com", + "phone_number":"+14165550123", + "residential_address":{ + "street":"123 King St", + "city":"Toronto", + "province":"ON", + "postal_code":"A1A 1A1" + }, + "id_type":"PASSPORT", + "id_number":"X1234567" + } + """ + Then the response status should be 201 + And the response JSON should contain fields 'application_id' and 'session_token' + + When I immediately resend the same POST to '/applications/start' with the same payload and CSRF + Then the response status should be 409 + And the error code should be 'DUPLICATE_APPLICATION' + + When I send a POST request to '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"PHONE_VERIFY"} + """ + And I send a POST request to '/auth/otp/verify' with JSON payload + """ + {"code":""} + """ + Then the response status should be 200 + + When I wait for autosave interval to elapse and reload the application page + Then only non-sensitive draft fields should be restored in the UI + And no ssn_last4, tokens, or PAN should be present in DOM or localStorage + + When I send a POST request to '/applications//financials' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "employment_status":"EMPLOYED", + "employer_name":"Aegis Ltd", + "gross_annual_income":85000.00, + "monthly_rent":1500.00, + "existing_debt_payments":300.00, + "sin_consent":true + } + """ + Then the response status should be 200 + And the response JSON should contain fields 'status' with value 'PENDING_REVIEW' and 'fico_pull_id' + + When I send a POST request to '/applications//financials' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "employment_status":"EMPLOYED", + "gross_annual_income":85000.00, + "monthly_rent":1500.00, + "existing_debt_payments":300.00, + "sin_consent":true + } + """ + Then the response status should be 400 + And the error field should indicate 'employer_name' is required + + When I send a POST request to '/applications//submit' with headers 'X-CSRF-Token' and 'X-App-Session' and JSON payload + """ + { + "card_product_id":"AEGIS_GOLD", + "e_signature":"SmFuZSBRIFB1YmxpYw==" + } + """ + 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 '**** **** **** ####' + And no full PAN should be present in the response or DOM + + When I send a POST request to '/auth/token/refresh' + Then the response status should be 200 + And new rotated refresh_token and access_token cookies should be set + + When I send a POST request to '/auth/token/refresh' reusing the previous refresh_token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + + When I send a PUT request to '/cards//pin' without 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + + When I send a PUT request to '/cards//pin' with headers 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"4321","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + + When I send a PUT request to '/cards//pin' with headers 'X-CSRF-Token' and JSON payload + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 200 + And the response JSON should contain 'updated_at' + + When I send a GET request to '/accounts//summary' + Then the response status should be 200 + And 'available_credit', 'credit_limit', and 'account_status' should be present + And rewards are excluded by default + + When I send a GET request to '/accounts//summary?include_rewards=true' + Then the response status should be 200 + And 'points_balance' should be present + + When I remain idle in the portal for 13 minutes + Then a session-timeout warning modal should appear + + When I remain idle until 15 minutes and invoke a protected API + Then the response status should be 401 + And the UI should be auto-logged out + + When I login again with MFA and fetch the application/account artifacts + Then previously created application and account are accessible + And no sensitive data is persisted in frontend storage + + @api + Scenario Outline: Registration Validations and Case-Insensitive Email Uniqueness + Given the API enforces registration field rules + When I send a POST request to '/auth/register' with JSON payload + """ + { + "first_name":"John", + "last_name":"Public", + "email":"", + "password":"", + "date_of_birth":"", + "phone_number":"", + "ssn_last4":"", + "agree_terms": + } + """ + Then the response status should be + And the response should contain or omit 'verification_token' as '' + And the error code should be '' + + Examples: + | email | password | date_of_birth | phone | ssn_last4 | agree_terms | status | verification_token_expected | error_code | + | new_user@example.com| Str0ngPass!2026 | 2008-01-01 | +14165550123 | 1234 | false | 400 | absent | TERMS_REQUIRED | + | new_user@example.com| Short1! | 2000-01-01 | +14165550123 | 1234 | true | 422 | absent | WEAK_PASSWORD | + | new_user@example.com| Str0ngPass!2026 | <17y_364d_ago> | +14165550123 | 1234 | true | 400 | absent | DOB_INVALID | + | new_user@example.com| Str0ngPass!2026 | 2000-01-01 | 4165550123 | 1234 | true | 400 | absent | PHONE_INVALID | + | new_user@example.com| Str0ngPass!2026 | 2000-01-01 | +14165550123 | 123 | true | 400 | absent | SSN_LAST4_INVALID | + | user@example.com | Str0ngPass!2026 | | +14165550123 | 1234 | true | 201 | present | | + | USER@EXAMPLE.COM | Str0ngPass!2026 | | +14165550123 | 1234 | true | 409 | absent | EMAIL_EXISTS | + + @api @auth + Scenario: Authentication Lockout, Rate Limit, Remember Me TTL, Logout Invalidation + Given the user 'user@example.com' exists with MFA enabled + When I perform 4 POST requests to '/auth/login' with wrong password within 60 seconds + Then each response status should be 401 + And the error code should be 'INVALID_CREDENTIALS' + When I perform a 5th POST to '/auth/login' with wrong password + Then the response status should be 403 + And the error code should be 'ACCOUNT_LOCKED' + When I attempt a correct login with valid TOTP during lockout + Then the response status should be 403 + And the error code should be 'ACCOUNT_LOCKED' + When I wait until 'unlock_at' plus 1 minute and login with remember_me true + Then the response status should be 200 + And refresh cookie expiry should be approximately 30 days + When I send 10 more login requests within a minute and a 11th attempt + Then the 11th response status should be 429 + And the error code should be 'RATE_LIMITED' + And the 'retry_after' header should be present + When I send a POST to '/auth/logout' with a valid 'X-CSRF-Token' + Then the response status should be 200 + And access and refresh cookies should be cleared + When I send POST '/auth/token/refresh' using the pre-logout refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I GET '/accounts//summary' without fresh login + Then the response status should be 401 + + @api @auth + Scenario: Refresh Token Rotation Concurrency Across Tabs and CSRF Session Binding + Given Tab A and Tab B share an initial authenticated session for 'user@example.com' + When Tab A sends POST '/auth/token/refresh' + Then Tab A receives 200 and rotated refresh_token R1-new + When Tab B sends POST '/auth/token/refresh' with the now-stale refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When Tab A GETs '/auth/csrf' and stores CSRF-A and sends POST '/auth/logout' with CSRF-A + Then Tab A receives 200 and cookies are expired + When Tab B sends POST '/auth/logout' using CSRF-A + Then the response status should be 403 + And the error code should be 'CSRF_INVALID' + When Tab B logs in and obtains CSRF-B then calls POST '/auth/token/refresh' twice in quick succession + Then the first refresh returns 200 and rotates tokens + And the second refresh returns 401 with 'TOKEN_INVALID' + And no tokens are present in localStorage or sessionStorage in either tab + + @api @csrf + Scenario: Access Control and CSRF Enforcement Matrix with SameSite=Strict + Given I am logged out + When I GET '/accounts//summary' without auth + Then the response status should be 401 + When I login and GET '/auth/csrf' and store CSRF-1 + And I GET '/accounts//summary' + Then the response status should be 200 + When I GET '/accounts//summary' + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When I POST '/accounts//transactions' without 'X-CSRF-Token' and JSON payload + """ + {"transaction_amount":5.00,"mcc_code":5999,"merchant_name":"Test","merchant_id":"T001","transaction_type":"PURCHASE"} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I retry the same POST with 'X-CSRF-Token' CSRF-1 + Then the response status should be 200 + And 'transaction_id' should be present + When a cross-site form POST is submitted from a different origin without credentials + Then the API returns 401 and no side effects occur + And all cookies have Secure, HttpOnly, SameSite=Strict flags + + @api @transactions + Scenario: Foreign Currency Purchase, Rewards, Pagination, CSRF, Rate Limit, Statements and Grace + Given I am authenticated with a valid 'X-CSRF-Token' and rewards baseline captured + And I connect to the WebSocket stream with a valid JWT and subscribe to 'account::transactions' + When I POST '/accounts//transactions' with 'X-CSRF-Token' and JSON payload + """ + { + "transaction_amount":100.00, + "currency_code":"USD", + "exchange_rate":1.250000, + "mcc_code":3000, + "merchant_name":"Aegis Air", + "merchant_id":"AA123", + "transaction_type":"PURCHASE", + "description":"Flight" + } + """ + Then the response status should be 200 + And 'foreign_fee_amount' should equal 3.75 and 'total_cad' should equal 128.75 + And a WebSocket event for the transaction should be received with masked PAN and no PII + When I disconnect and reconnect the WebSocket without JWT + Then the connection is rejected with 401 or 403 + When I POST another PURCHASE in CAD with JSON payload + """ + { + "transaction_amount":10.99, + "mcc_code":5999, + "merchant_name":"Shop", + "merchant_id":"S001", + "transaction_type":"PURCHASE" + } + """ + Then the response status should be 200 + And rewards expected increment is Travel=floor(128.75*3)=386 and Other=floor(10.99*1)=10 + When I POST a transaction without 'X-CSRF-Token' + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I GET '/accounts//transactions?from_date=&to_date=&page=1&per_page=100&category=PURCHASE' + Then the response status should be 200 + And 'transactions', 'total_count', and 'total_pages' should be present + When I GET '/accounts//transactions' + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When I rapidly POST 10 more small PURCHASES within 60 minutes + Then all 10 responses should be 200 + When I POST an 11th within the same window + Then the response status should be 429 + And the error code should be 'FREQ_EXCEEDED' + And 'mfa_required' is true and 'Retry-After' header is present + When I GET '/accounts//statements/?format=JSON' after cycle close + Then 'total_spend' equals the sum of transaction amounts within ±0.01 + And if 'prev_statement_balance_paid_in_full' is true then 'interest_charged' equals 0 else it matches REQ-009 formula rounded to cents + When I GET '/accounts//statements/?format=PDF' + Then the response status should be 200 and content type is application/pdf + + @api @transactions + Scenario Outline: Transaction Field Validation and FX Precision + Given available_credit is at least $50.00 + When I POST '/accounts//transactions' with JSON payload + """ + { "transaction_amount": , "currency_code": "", "exchange_rate": , "mcc_code": , "merchant_name": "X", "merchant_id":"Y", "transaction_type":"PURCHASE" } + """ + Then the response status should be + And the error code should be '' + + Examples: + | amount | currency | rate | mcc | status | error_code | + | 0.00 | CAD | null | 5999 | 422 | INVALID_AMOUNT | + | 5.00 | USD | null | 5999 | 400 | EXCHANGE_RATE_MISSING| + | 1.23 | USD | 1.3333337 | 3000 | 400 | EXCHANGE_RATE_PREC | + + @api @transactions + Scenario: Valid Small-Value FX with Rounding and Listing Page Validation + Given available_credit baseline is recorded + When I POST '/accounts//transactions' with JSON payload + """ + { + "transaction_amount":1.23, + "currency_code":"USD", + "exchange_rate":1.333333, + "mcc_code":3000, + "merchant_name":"MiniTravel", + "merchant_id":"MT001", + "transaction_type":"PURCHASE" + } + """ + Then the response status should be 200 + And 'foreign_fee_amount' equals 0.05 and 'total_cad' equals 1.69 + When I POST '/accounts//transactions' with JSON payload + """ + {"transaction_amount":10.00,"mcc_code":6010,"merchant_name":"CashPoint","merchant_id":"CA001","transaction_type":"CASH_ADVANCE"} + """ + Then the response status should be 200 + When I POST '/accounts//transactions' with JSON payload + """ + {"transaction_amount":15.00,"mcc_code":6012,"merchant_name":"BalanceXfer","merchant_id":"BT001","transaction_type":"BALANCE_TRANSFER"} + """ + Then the response status should be 200 + When I GET '/accounts//transactions?page=0&per_page=25' + Then the response status should be 400 + And the error describes 'page must be >= 1' + When I GET '/accounts//transactions?page=1&per_page=25&category=PURCHASE' + Then the response status should be 200 + + @api @transactions + Scenario: Transactions Date/Time Boundaries, UTC/DST, per_page Max, Stable Pagination + Given I have created transactions at T1=2026-03-14T00:00:00Z, T2=2026-03-14T23:59:59Z, T3=2026-03-15T12:00:00Z + When I GET '/accounts//transactions?from_date=2026-03-14&to_date=2026-03-14&page=1&per_page=25' + Then results include T1 and T2 only + When I GET '/accounts//transactions?from_date=2026-03-15&to_date=2026-03-15&page=1&per_page=25' + Then results include T3 only + When I ensure UTC around DST by creating transactions at 2026-03-08T01:59:59Z and 2026-03-08T03:00:01Z + And I GET '/accounts//transactions?from_date=2026-03-08&to_date=2026-03-08' + Then both are included + When I GET '/accounts//transactions?per_page=101' + Then the response status should be 400 + And the error describes 'per_page max 100' + When I populate more than 25 transactions and list page 1 and page 2 with per_page=25 + Then no duplicate items appear across pages and ordering is stable + And response contains 'total_count','page','total_pages' + And masked PAN is shown and no PII leaked + + @api @rewards + Scenario: Rewards Accrual Boundary and MCC Classification with Floor Rounding + Given I capture baseline points P0 via GET '/accounts//summary?include_rewards=true' + When I POST four CAD PURCHASES for MCC 3000 with 1.00, MCC 3000 with 0.99, MCC 5999 with 1.00, MCC 5999 with 0.01 + Then each response status should be 200 + When I POST a transaction with invalid mcc_code '123' (3 digits) + Then the response status should be 400 + When I GET '/accounts//transactions?category=PURCHASE&page=1&per_page=25' + Then the four purchases are present + When I GET '/accounts//summary?include_rewards=true' + Then points increment equals 6 over P0 (or appears on next statement per accrual policy) + When I GET '/accounts//statements/' + Then rewards_earned includes +6 and total_spend reconciles to ±0.01 + + @api @transactions @mfa + Scenario: Transaction Rate-Limit Recovery via MFA + Given I have posted 10 small PURCHASES within 60 minutes successfully + When I POST an 11th transaction within the same window + Then the response status should be 429 + And 'mfa_required' is true and 'Retry-After' header is present + When I POST '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"TRANSACTION_RATE_LIMIT"} + """ + And I POST '/auth/otp/verify' with JSON payload + """ + {"code":""} + """ + Then the response status should be 200 + When I retry the previously blocked transaction + Then the response status should be 200 + And available_credit is updated accordingly + When I submit an invalid OTP before success in a new attempt + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @realtime + Scenario: WebSocket Live Feed Resilience and Authorization + Given I connect to the WebSocket with Authorization: Bearer and subscribe to 'account::transactions' + Then I receive a subscription ack + When I POST a CAD PURCHASE to '/accounts//transactions' + Then a single WebSocket event is received with masked PAN and correct amounts + When I keep the socket open until JWT expiry and send a ping + Then the server closes the connection or emits an unauthorized event + When I POST '/auth/token/refresh' to obtain a new access_token and reconnect the socket and resubscribe + Then I receive a new subscription ack and heartbeats + When I attempt to subscribe to 'account::transactions' + Then subscription is rejected with 403 FORBIDDEN and no events delivered + When I attempt to pass JWT in query string on connect + Then the connection is rejected + And all frames remain over TLS 1.3 without downgrade + + @api @card + Scenario: Card Controls Freeze/Unfreeze with OTP and CSRF, Transactions Blocked While Frozen + Given account summary shows card is Active + When I PATCH '/cards//status' to Frozen with headers 'X-CSRF-Token' and JSON payload + """ + {"status":"Frozen","reason":"Travel","confirm_otp":""} + """ + Then the response status should be 200 + And 'new_status' is 'Frozen' + When I POST '/accounts//transactions' while card is Frozen + Then the response status should be 403 + And the error code should be 'CARD_INACTIVE' + When I PATCH '/cards//status' to Active with valid OTP and CSRF + Then the response status should be 200 + And 'new_status' is 'Active' + When I attempt unfreeze with wrong OTP + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @card + Scenario: Report Card STOLEN with Delivery Address Override, OTP, Audit, Irreversibility + Given card is Active and masked PAN is visible in summary only + When I POST '/cards//report-lost' without 'X-CSRF-Token' + """ + {"loss_type":"STOLEN"} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I POST '/auth/otp/request' with JSON payload + """ + {"channel":"SMS","purpose":"CARD_REPORT_STOLEN"} + """ + And I POST '/cards//report-lost' with headers 'X-CSRF-Token' and JSON payload + """ + { + "loss_type":"STOLEN", + "confirm_otp":"", + "delivery_address":{"street":"123 King","city":"Toronto","province":"Ontario","postal_code":"12345"} + } + """ + Then the response status should be 400 + And address validation errors for 'province' and 'postal_code' are returned + When I POST '/cards//report-lost' with valid address override and last_known_use + """ + { + "loss_type":"STOLEN", + "confirm_otp":"", + "delivery_address":{"street":"123 King","city":"Toronto","province":"ON","postal_code":"A1A 1A1"}, + "last_known_use":"2026-03-01T12:00:00Z" + } + """ + Then the response status should be 200 + And 'blocked_card_id','new_card_eta','case_number' are present + When I PATCH '/cards//status' to Active or Frozen after Blocked + Then the response status should be 400 + And the error code should be 'INVALID_TRANSITION' + When I PUT '/cards//pin' while Blocked + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CARD_BLOCKED' + When I POST '/cards//report-lost' again + Then the response status should be 409 + And the error code should be 'ALREADY_BLOCKED' + + @api @payments @billing + Scenario: Payments, Late Fee, Interest/Grace, Scheduling, CSRF, Rescind Window + Given I GET '/accounts//statements/' and capture due_date, minimum_payment_due, total_balance + When system time is advanced to due_date + 3 days and I GET the statement + Then 'late_fee' equals 35.00 + When I POST '/accounts//payments' with 'X-CSRF-Token' and JSON payload + """ + {"payment_type":"MINIMUM","payment_amount":49.99,"bank_account_id":"00000000-0000-0000-0000-000000000000"} + """ + Then the response status should be 422 + And the error code should be 'INVALID_BANK_ACCOUNT' + When I POST '/accounts//payments' with JSON payload + """ + {"payment_type":"MINIMUM","payment_amount":45.00,"bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 400 + And the error code should be 'BELOW_MINIMUM' + When I POST a valid FULL_BALANCE payment + """ + {"payment_type":"FULL_BALANCE","bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 200 + And 'payment_id' is present + When I POST a scheduled payment with past scheduled_date + """ + {"payment_type":"CUSTOM","payment_amount":10.00,"scheduled_date":"2000-01-01","bank_account_id":"11111111-2222-3333-4444-555555555555"} + """ + Then the response status should be 400 + And the error code should be 'INVALID_SCHEDULED_DATE' + When I POST '/auth/token/refresh' and then POST '/auth/token/refresh' again using the same refresh token + Then the second response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I POST '/accounts/' DELETE to exercise Right to Rescind within 14 days with 'X-CSRF-Token' + Then the response status should be 200 + And subsequent GET '/accounts//summary' returns 404 or 403 + + @api @payments + Scenario Outline: Payments CUSTOM Boundary and CSRF + Given total_balance and an active bank_account_id are known + When I POST '/accounts//payments' with headers '' and JSON payload + """ + {"payment_type":"CUSTOM","payment_amount":,"bank_account_id":"","scheduled_date":} + """ + Then the response status should be + And the error code should be '' + + Examples: + | csrf_header | amount | bank_id | scheduled | status | error_code | + | X-CSRF-Token | 1.00 | 11111111-2222-3333-4444-555555555555 | null | 200 | | + | X-CSRF-Token | 0.999 | 11111111-2222-3333-4444-555555555555 | null | 400 | SCALE_INVALID | + | X-CSRF-Token | | 11111111-2222-3333-4444-555555555555 | null | 400 | ABOVE_MAX | + | X-CSRF-Token | | 11111111-2222-3333-4444-555555555555 | "tomorrow" | 200 | | + | (missing) | 5.00 | 11111111-2222-3333-4444-555555555555 | null | 403 | CSRF_MISSING | + + @api @statements + Scenario: Statement Calculations Boundary: Interest Rounding, Grace, Late Fee UTC Boundary, Not Found + Given I GET the previous statement to read 'prev_statement_balance_paid_in_full' and 'due_date' + When I set prev_statement_balance_paid_in_full to true and GET current statement after cycle close + Then 'interest_charged' equals 0 + When I set prev_statement_balance_paid_in_full to false with $0.01 remainder and GET current statement + Then 'interest_charged' matches (ADB × APR / 365) × Days formula rounded to cents + And 'total_spend' equals the sum of transactions within ±0.01 + When payment_received_date is exactly due_date + 2 days 23:59:59Z and I GET recalculated statement + Then 'late_fee' equals 0 + When payment_received_date is due_date + 2 days + 1 second and I GET recalculated statement + Then 'late_fee' equals 35.00 + When I GET '/accounts//statements/' + Then the response status should be 404 + + @api @applications + Scenario: Application Step Order Enforcement, Session Token Validation, PENDING and DECLINED + Given userA starts Step 1 via POST '/applications/start' and receives application_id_A and session_token_A + When userA POSTs '/applications//submit' skipping Step 2 with valid e_signature + Then the response status should be 400 + And the error code should be 'INVALID_STEP_ORDER' + When userA POSTs '/applications//financials' with tampered 'X-App-Session' + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When userA POSTs Step 2 with employment_status EMPLOYED but missing employer_name + Then the response status should be 400 + When corrected Step 2 is submitted with sin_consent true and bureau is configured for FICO=620 + Then the response status should be 200 + And status is 'PENDING_REVIEW' + When userA submits Step 3 with valid e_signature and marketing_opt_in omitted + Then decision is 'PENDING' and marketing_opt_in defaults to false + When userB completes Step 1 and Step 2 with FICO=580 and submits Step 3 with malformed e_signature + Then the response status should be 400 + And the error code should be 'SIGNATURE_REQUIRED' + When userB corrects e_signature and resubmits + Then the decision is 'DECLINED' with 'reason_code' + When userB attempts to start another application immediately + Then the response status should be 409 or 201 per business rule + And no cross-user data leakage occurs + + @api @applications + Scenario: Application Step 2 Credit Pull: sin_consent Enforcement, 503 Retry Idempotency, Session Expiry + Given application_id and session_token for user@example.com are available + When I POST Step 2 with EMPLOYED and missing employer_name + Then the response status should be 400 + And field 'employer_name' is required + When I POST Step 2 with employer_name but sin_consent=false + Then the response status should be 400 + And error indicates sin_consent must be true + When I POST Step 2 valid and bureau returns 503 + Then the response status should be 200 + And status is 'PENDING_REVIEW' with a 'fico_pull_id' queued + When I resubmit identical Step 2 + Then the response status should be 200 + And the same 'fico_pull_id' is returned (idempotent) + When I POST Step 2 with a tampered X-App-Session + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When the session_token expires and I retry Step 2 + Then the response status should be 401 + And the error code should be 'SESSION_EXPIRED' + When I obtain a fresh session_token and submit Step 2 then Step 3 with FICO=650 + Then the decision is 'PENDING' + + @api @applications @privacy + Scenario: Application Step 1 Validation, Autosave Privacy and Cross-User Isolation + Given userA is logged in with CSRF token and opens Step 1 + When userA POSTs Step 1 with email userB@example.com + Then the response status should be 400 + And error 'email must match authenticated user' + When userA POSTs Step 1 with invalid province 'Ontario' + Then the response status should be 400 + And field 'address.province' invalid + When userA POSTs Step 1 with invalid postal_code '12345' + Then the response status should be 400 + And field 'address.postal_code' invalid + When userA POSTs Step 1 with id_type 'NATIONAL_ID' + Then the response status should be 400 + When userA POSTs Step 1 with id_type DRIVERS_LICENSE and overlength id_number + Then the response status should be 400 + When userA submits valid Step 1 + Then the response status should be 201 + And 'application_id' and 'session_token' are returned + When autosave triggers and the page reloads + Then only non-sensitive fields are restored and no ssn_last4 or tokens appear in DOM/storage + When userA logs out and userB logs in + Then userB sees no draft from userA + When userB attempts to POST '/applications/start' again immediately + Then the response status should be 409 + And the error code should be 'DUPLICATE_APPLICATION' + When userA attempts to POST '/applications/start' again + Then the response status should be 409 + And no cross-user leakage of application_id is observed + + @api @webhook @notifications + Scenario: Notifications Webhook Validation, PII Masking, Severity Rendering + Given the notifications webhook endpoint is reachable + When I POST '/notifications/webhook' with JSON payload + """ + {"account_id":"","alert_type":"UNKNOWN_EVENT","channel":"IN_APP","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 400 + And the error code should be 'INVALID_ALERT_TYPE' + When I POST '/notifications/webhook' with invalid channel + """ + {"account_id":"","alert_type":"LATE_PAYMENT","channel":"PAGER","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 400 + When I POST a valid STATEMENT_READY IN_APP alert + """ + {"account_id":"","alert_type":"STATEMENT_READY","channel":"IN_APP","message_body":"Your statement is ready","severity":"INFO","idempotency_key":""} + """ + Then the response status should be 200 + When I POST an OVER_LIMIT EMAIL alert with masked PAN + """ + {"account_id":"","alert_type":"OVER_LIMIT","channel":"EMAIL","message_body":"Over-limit used on card **** **** **** 1234","severity":"WARNING","idempotency_key":""} + """ + Then the response status should be 200 + When I POST a FRAUD_FLAG IN_APP alert containing a PAN-like string + """ + {"account_id":"","alert_type":"FRAUD_FLAG","channel":"IN_APP","message_body":"Suspicious charge on card 4111 1111 1111 1111 at Merchant X","severity":"CRITICAL","idempotency_key":""} + """ + Then the response status should be 200 + And stored/rendered message masks to '**** **** **** 1111' + When I POST an alert with message_body > 500 chars + Then the response status should be 400 + When I resend STATEMENT_READY with a different idempotency_key and same content + Then a distinct alert is created + + @api @transactions + Scenario: Transaction Currency and Field Validation with Success at Precision Boundaries + Given available_credit baseline is recorded + When I POST with invalid currency_code + """ + {"transaction_amount":10.00,"currency_code":"USDX","exchange_rate":1.250000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with USD and zero exchange_rate + """ + {"transaction_amount":5.00,"currency_code":"USD","exchange_rate":0.000000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with USD and negative exchange_rate + """ + {"transaction_amount":5.00,"currency_code":"USD","exchange_rate":-1.230000,"mcc_code":3000,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST with CAD amount scale > 2 + """ + {"transaction_amount":10.999,"mcc_code":5999,"merchant_name":"A","merchant_id":"B","transaction_type":"PURCHASE"} + """ + Then the response status should be 400 + When I POST a valid EUR FX at max precision + """ + {"transaction_amount":2.50,"currency_code":"EUR","exchange_rate":0.999999,"mcc_code":3000,"merchant_name":"EuroTravel","merchant_id":"ET001","transaction_type":"PURCHASE"} + """ + Then the response status should be 200 + And 'foreign_fee_amount' and 'total_cad' are correctly rounded and available_credit decremented + When I GET '/accounts//transactions?category=GIFT' + Then the response status should be 400 + When I GET '/accounts//transactions?category=PURCHASE&page=1&per_page=25' + Then the response status should be 200 + When I GET '/accounts//transactions' + Then the response status should be 403 + + @api @csrf @auth + Scenario: CSRF Regeneration After Refresh Rotation + Given I GET '/auth/csrf' and store CSRF-0 + When I POST '/accounts//transactions' with CSRF-0 + """ + {"transaction_amount":1.00,"mcc_code":5999,"merchant_name":"T","merchant_id":"T01","transaction_type":"PURCHASE"} + """ + Then the response status should be 200 + When I POST '/auth/token/refresh' + Then the response status should be 200 + When I PUT '/cards//pin' using stale CSRF-0 + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_INVALID' or 'CSRF_MISSING' + When I GET '/auth/csrf' and store CSRF-1 + Then CSRF-1 differs from CSRF-0 + When I PUT '/cards//pin' with CSRF-1 and mismatched pins + """ + {"new_pin":"1234","confirm_pin":"4321","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + When I PATCH '/cards//status' to Frozen with CSRF-1 and valid OTP + """ + {"status":"Frozen","confirm_otp":""} + """ + Then the response status should be 200 + When I POST '/auth/token/refresh' reusing the previous refresh token + Then the response status should be 401 + And the error code should be 'TOKEN_INVALID' + When I PATCH '/cards//status' to Active with CSRF-1 and valid OTP + Then the response status should be 200 + + @api @otp + Scenario: Phone OTP Verification Flow with Expiry, Resend Throttle, Attempts Remaining + Given I POST '/auth/otp/request' with + """ + {"channel":"SMS","purpose":"PHONE_VERIFY"} + """ + Then the response status should be 200 + When I POST '/auth/otp/verify' with non-numeric code + """ + {"code":"12A45B"} + """ + Then the response status should be 400 + When I POST '/auth/otp/verify' with wrong code '000000' + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I POST '/auth/otp/request' twice within 60 seconds + Then the second response status should be 429 or 400 per throttle policy + When I wait for expiry and POST '/auth/otp/verify' with an expired but correct code + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I POST '/auth/otp/request' again and then verify with the new correct code + Then the response status should be 200 + And 'phone_verified' is true + When I reuse the same OTP code again + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + + @api @pin + Scenario: Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF + Given card is not Blocked and I have a valid session + When I PUT '/cards//pin' without 'X-CSRF-Token' + """ + {"new_pin":"1234","confirm_pin":"1234","session_otp":""} + """ + Then the response status should be 403 + And the error code should be 'CSRF_MISSING' + When I PUT with whitespace in PIN + """ + {"new_pin":"12 4","confirm_pin":"12 4","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_FORMAT' + When I PUT with non-numeric PIN + """ + {"new_pin":"12A4","confirm_pin":"12A4","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_FORMAT' + When I PUT with 3-digit PIN and then 5-digit PIN + """ + {"new_pin":"123","confirm_pin":"123","session_otp":""} + """ + Then the response status should be 400 + When I PUT with 5-digit PIN + """ + {"new_pin":"12345","confirm_pin":"12345","session_otp":""} + """ + Then the response status should be 400 + When I PUT with leading zeros but expired OTP + """ + {"new_pin":"0000","confirm_pin":"0000","session_otp":""} + """ + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When I request a fresh OTP and PUT with mismatched confirm_pin + """ + {"new_pin":"0000","confirm_pin":"0001","session_otp":""} + """ + Then the response status should be 400 + And the error code should be 'PIN_MISMATCH' + When I submit two more attempts with wrong OTPs until attempts throttle + Then the response status should be 401 + And the error code should be 'OTP_FAILED' + When after cooldown I PUT with valid OTP and matching '0000' + Then the response status should be 200 + And no PIN or OTP appears in any UI or storage + + @api @admin @audit + Scenario: Audit Trail for Credit Limit Changes and Access Control + Given as cardholder I GET '/accounts//summary' and record L0, A0, B0 + When as admin I PATCH '/admin/accounts//credit-limit' with + """ + {"credit_limit": ""} + """ + Then the response status should be 200 + When as cardholder I GET '/accounts//summary' + Then 'credit_limit' equals L0+1000 and 'available_credit' increased accordingly + When as admin I GET '/admin/audit?entity=credit_limit&account_id=' + Then an audit record exists with old_value L0 and new_value L0+1000 and immutable metadata + When as cardholder I GET the same audit endpoint + Then the response status should be 403 + And the error code should be 'FORBIDDEN' + When as cardholder I try PATCH '/accounts//credit-limit' + Then the response status should be 404 or 403 + And PAN masking is enforced in any payloads + When as admin I patch credit_limit down by 500 and re-check summary and audit + Then a second immutable audit entry exists and summary reflects the new limit + + @api @rescind @realtime + Scenario: Right to Rescind Security Sweep Post-Delete + Given the account is within 14 days of issuance and Active + And I have an active WebSocket subscription to 'account::transactions' + When I POST a small PURCHASE and observe one realtime event + Then the event contains masked PAN only + When I DELETE '/accounts/' with 'X-CSRF-Token' + Then the response status should be 200 + When I GET '/accounts//summary' after rescind + Then the response status should be 404 or 403 + When I POST '/accounts//transactions' after rescind + Then the response status should be 403 + When I POST '/accounts//payments' after rescind + Then the response status should be 403 + And the WebSocket is terminated or receives an unauthorized event + When I POST '/notifications/webhook' targeting the rescinded account + """ + {"account_id":"","alert_type":"STATEMENT_READY","channel":"IN_APP","message_body":"Test","severity":"INFO","idempotency_key":""} + """ + Then the response is a safe suppression (404 or 200 no-op) and no in-portal alert appears + When I DELETE '/accounts/' again + Then the response is idempotent (404 NOT_FOUND or 409 ALREADY_CLOSED) + + # UI Tests + + @ui @session + Scenario: Session Timeout Warning - Stay Signed In Flow and Auto-Logout + Given I am on the portal and logged in with MFA with Secure, HttpOnly, SameSite=Strict cookies set + And I have an active application Step 1 draft saved + When I remain idle for 13 minutes + Then I should see a session-timeout warning modal with 2-minute countdown + When I click the 'Stay Signed In' button + Then my session should remain active and a silent token refresh should occur + And I can proceed to Step 2 without re-authenticating + When I reload the page + Then non-sensitive draft fields should be restored and ssn_last4 should not be present in localStorage or the DOM + When I ignore the warning on a subsequent cycle and remain idle past 15 minutes + Then I should be logged out automatically + And any protected API requests should result in 401 + + @ui @pci + Scenario: PAN Masking and Iframe Tokenization Compliance Across Portal Surfaces + Given I am on the account dashboard + Then I should see the card number masked as '**** **** **** 1234' + And no full PAN appears in any DOM element or data attribute + When I open browser devtools and review Network responses + Then no API response contains a full PAN and caching does not store PAN + And localStorage and sessionStorage contain no PAN, ssn_last4, or tokens + When I navigate to card management + Then any card input fields are within a third-party iframe tokenization component loaded over TLS 1.3 + And the host page never receives raw PAN + When a WebSocket transaction event arrives + Then the payload shows masked PAN only and no tokens or PII + When I open the latest statement JSON and PDF + Then no full PAN is present in either format + When a notification is rendered that originally contained a PAN-like string + Then the UI displays a masked PAN and no full PAN appears anywhere + And CSP headers restrict iframe/script sources appropriately diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.json b/functional_tests/functional-test-aegis/functional-test-aegis.json new file mode 100644 index 0000000..7725556 --- /dev/null +++ b/functional_tests/functional-test-aegis/functional-test-aegis.json @@ -0,0 +1,1027 @@ +[ + { + "type": "e2e-functional", + "title": "Applicant Journey E2E: Register -> Verify -> Login (MFA) -> Application Steps -> APPROVED -> Set PIN -> Summary -> Token Rotation, CSRF, Autosave, Session Timeout", + "description": "Covers full new-user onboarding with regulatory controls: registration validations, email verification, OAuth2 login with TOTP, multi-step credit application with autosave and session_token, approval path, masked PAN, CSRF enforcement, token refresh rotation/reuse detection, session timeout warning and logout.", + "testId": "TC-E2E-001", + "testDescription": "Validates end-to-end user lifecycle from registration through approved decision and card PIN setup while asserting security controls (TLS1.3, CSRF, cookie flags, JWT rotation) and UI masking.", + "prerequisites": "No existing account for user@example.com; test device_id available; MFA seed provisioned for TOTP; products catalog includes AEGIS_GOLD; test phone +14165550123 can receive OTP.", + "stepsToPerform": "1. Ensure browser connects over TLS 1.3 to https://portal.aegiscard.com and API https://api.aegiscard.com/v2; verify WAF and HSTS present.\n2. Call POST /v2/auth/register with valid first_name, last_name, unique email user@example.com, strong password (>=12 chars with upper/lower/digit/symbol), date_of_birth exactly 18 years ago today (boundary), phone_number +14165550123 (E.164), ssn_last4 1234, agree_terms true.\n3. Verify 201 response with user_id and verification_token; confirm password complexity not rejected and email uniqueness enforced.\n4. ASSUMPTION: Complete email verification by calling POST /v2/auth/verify with verification_token; expect 200 and account marked verified.\n5. Call POST /v2/auth/login with email user@example.com, correct password, device_id UUIDv4, remember_me true; then submit valid 6-digit TOTP mfa_code.\n6. Confirm 200 with access_token and refresh_token set in HttpOnly, Secure, SameSite=Strict cookies; verify no tokens present in localStorage or JS-accessible scope.\n7. ASSUMPTION: Obtain CSRF token via GET /v2/auth/csrf and store value for subsequent state-changing requests.\n8. Start application Step 1 by POST /v2/applications/start (with X-CSRF-Token) including full_legal_name 'Jane Q Public', email matching user@example.com, phone_number +14165550123, residential_address fields valid (Canadian postal code boundary like A1A 1A1), id_type PASSPORT, id_number 'X1234567'.\n9. Verify 201 with application_id and session_token; confirm duplicate Step 1 immediately repeated returns 409 DUPLICATE_APPLICATION.\n10. ASSUMPTION: Trigger phone OTP and verify by calling POST /v2/auth/otp/request then POST /v2/auth/otp/verify with correct code; expect 200 and phone_verified=true.\n11. Wait until the UI auto-saves after 60s; reload the page; assert draft restored for non-sensitive fields and that ssn_last4, tokens, or full PAN are not present in localStorage/DOM.\n12. Proceed to Step 2 by POST /v2/applications/{application_id}/financials with X-App-Session header set to session_token, employment_status EMPLOYED, employer_name 'Aegis Ltd', gross_annual_income 85000.00, other_income omitted, monthly_rent 1500.00, existing_debt_payments 300.00, sin_consent true; include X-CSRF-Token.\n13. Verify 200 with status PENDING_REVIEW and fico_pull_id present; assert error 400 if employer_name missing when EMPLOYED by retrying without employer_name (negative) and receiving 400 then re-submit correct data to restore positive state.\n14. Immediately POST /v2/applications/{application_id}/submit with valid card_product_id AEGIS_GOLD, e_signature Base64 of 'Jane Q Public', marketing_opt_in omitted; include X-CSRF-Token and X-App-Session.\n15. Verify decision APPROVED (FICO > 680) with credit_limit returned and card_number_masked like **** **** **** 1234; ensure no full PAN in response or DOM (REQ-014).\n16. Call POST /v2/auth/token/refresh; verify 200 new access_token and rotated refresh_token; attempt to reuse the old refresh_token and expect 401 TOKEN_INVALID (single-use rotation).\n17. Attempt PUT /v2/cards/{card_id}/pin without X-CSRF-Token and expect 403 (ASSUMPTION: error CSRF_MISSING); verify no PIN change occurred.\n18. Retry PUT /v2/cards/{card_id}/pin with X-CSRF-Token and valid session_otp, new_pin 1234, confirm_pin 1234; expect 200 and updated_at timestamp; attempt with mismatch 1234/4321 returns 400 PIN_MISMATCH (negative) then correct it.\n19. GET /v2/accounts/{account_id}/summary; verify owner-only access, balances, available_credit, credit_limit, account_status Active, and that include_rewards=false by default; retry with include_rewards=true and verify points_balance present.\n20. Idle on the portal without activity until 13 minutes; verify session timeout warning modal appears; continue idling to 15 minutes; assert auto-logout; protected API returns 401.\n21. Login again with MFA; verify all previously created artifacts (application, account) remain accessible; no sensitive data persisted in frontend storage; logout via ASSUMPTION: POST /v2/auth/logout and confirm cookies cleared.", + "expectedResult": "User registers and verifies email; logs in with MFA; completes application steps in order; receives APPROVED decision; masked PAN only shown; PIN set succeeds only with CSRF and OTP; token refresh rotates and old refresh token reuse is rejected; session timeout warning and auto-logout work; all mutating endpoints enforce CSRF; no sensitive data appears in localStorage/DOM.", + "apiPath": "/v2/auth/register, /v2/auth/verify, /v2/auth/login, /v2/auth/token/refresh, /v2/auth/logout, /v2/auth/csrf, /v2/auth/otp/request, /v2/auth/otp/verify, /v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, POST, POST, POST, GET, POST, POST, POST, POST, POST, PUT, GET", + "endpointGroup": "Auth, Applications, Card Management, Accounts", + "workflow": "E2E, Authentication, Credit Application, Card Management, Session Management", + "businessRuleIds": "REQ-014, NFR-05, NFR-06", + "calculationFormula": "N/A for decision logic here; decision per FICO thresholds handled by backend per SRS (APPROVED if FICO > 680).", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms,full_legal_name,residential_address,id_type,id_number,employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature,new_pin,confirm_pin,session_otp", + "validationRules": "Email RFC 5322 unique, Password >=12 with complexity, DOB >=18 including leap-year birthdays, Phone E.164, Postal code Canadian format, employer_name required when EMPLOYED, e_signature Base64, PIN exactly 4 digits", + "errorCodesCovered": "EMAIL_EXISTS, WEAK_PASSWORD, INVALID_CREDENTIALS, ACCOUNT_LOCKED, RATE_LIMITED, SESSION_EXPIRED, DUPLICATE_APPLICATION, SIGNATURE_REQUIRED, TOKEN_INVALID, OTP_FAILED, PIN_MISMATCH, PIN_FORMAT, CSRF_MISSING", + "stateTransitions": "Application: NEW->STEP1->STEP2->SUBMITTED->APPROVED; Card PIN: unset->set; Session: active->warning(13m)->expired(15m)", + "alertTypesCovered": "ASSUMPTION: In-app verification notices on approval", + "dataMaskingChecks": "No full PAN in responses or DOM; masked PAN **** **** **** 1234 only; no tokens or ssn_last4 in localStorage", + "auditTrailChecks": "ASSUMPTION: Verify audit record for application creation and decision, user_id/session_id/ip logged", + "piiFields": "full_legal_name,email,phone_number,residential_address,date_of_birth,ssn_last4", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/verify exists for email verification; ASSUMPTION: /v2/auth/csrf issues CSRF token; ASSUMPTION: OTP endpoints exist; ASSUMPTION: CSRF error code CSRF_MISSING with 403", + "testData": "email user@example.com, phone +14165550123, device_id 550e8400-e29b-41d4-a716-446655440000, e_signature Base64 'SmFuZSBRIFB1YmxpYw=='", + "cleanupSteps": "Logout; if needed, delete test user via admin tool (non-prod); revoke tokens", + "dependencies": "Products catalog contains AEGIS_GOLD; MFA seed provisioned; OTP delivery channel reachable" + }, + { + "type": "functional", + "title": "Foreign Currency Purchase, Rewards, Statement Accuracy, WebSocket Feed, Pagination and Rate Limit", + "description": "Validates non-CAD transaction with exchange_rate, foreign fee and Total_CAD math, rewards floor rounding by MCC, statement accuracy and grace logic, WebSocket event delivery, list transactions pagination/filtering, CSRF enforcement, and transaction rate-limit MFA trigger.", + "testId": "TC-TXN-FOREIGN-002", + "testDescription": "Ensures REQ-006 foreign fee math, REQ-012/REQ-013 rewards floor, REQ-015 statement accuracy, grace period check, and realtime feed operate correctly; covers invalid date range and CSRF missing negatives plus rate-limit behavior (>10 txns/60min).", + "prerequisites": "Approved and Active account_id for user@example.com with sufficient available_credit; authenticated session with valid access_token and X-CSRF-Token; WebSocket JWT available.", + "stepsToPerform": "1. Open WebSocket wss://realtime.aegiscard.com/v2/stream with valid JWT; subscribe to account transaction topic; expect subscription ack.\n2. POST /v2/accounts/{account_id}/transactions with X-CSRF-Token to create USD PURCHASE: transaction_amount 100.00, currency_code USD, exchange_rate 1.250000, mcc_code 3000 (Travel), merchant_name 'Aegis Air', merchant_id 'AA123', description 'Flight'.\n3. Verify 200 response includes transaction_id, available_credit updated, auth_code; assert foreign_fee_amount = (100.00 * 1.250000 * 0.03) = 3.75 and Total_CAD = (100.00 * 1.250000) * 1.03 = 128.75 with proper precision rounding (2 decimals for amounts).\n4. Validate WebSocket receives an event for the transaction with correct masked PAN and amounts; no PII leakage; unauthorized socket attempt (disconnect and reconnect without JWT) is rejected (401/403).\n5. POST another PURCHASE in non-Travel category amount 10.99 CAD, mcc_code 5999, merchant_name 'Shop', to test rewards rounding; expect approval.\n6. Compute expected rewards: Travel points = floor(128.75 * 3) = 386, Other points = floor(10.99 * 1) = 10; verify points increment on statement later (REQ-012, REQ-013). ASSUMPTION: Rewards accrue on total posted CAD amounts.\n7. Attempt a transaction without X-CSRF-Token; expect 403 CSRF_MISSING and no transaction created (negative CSRF enforcement).\n8. GET /v2/accounts/{account_id}/transactions with invalid date range (to_date earlier than from_date); expect 400 INVALID_DATE_RANGE.\n9. GET /v2/accounts/{account_id}/transactions with from_date=current cycle start, to_date=now, page=1, per_page=100 (boundary), category filter PURCHASE; expect 200 with transactions[], total_count, total_pages; verify owner-only access by requesting another account_id and expecting 403 FORBIDDEN.\n10. Rapidly initiate 10 more small PURCHASE transactions within 60 minutes (with CSRF) and ensure approvals; attempt an 11th in the same 60-min window; expect 429 FREQ_EXCEEDED with mfa_required true; verify Retry-After header present (ASSUMPTION).\n11. GET /v2/accounts/{account_id}/statements/{statement_id} after cycle close (ASSUMPTION: use latest statement id); format=JSON; validate total_spend equals sum(transaction_amount[]) within ±$0.01 tolerance (REQ-015).\n12. Validate REQ-010 grace period: if prev_statement_balance_paid_in_full=true, confirm interest_charged=0 for this cycle; else compute interest via REQ-009 using Interest = (ADB × APR / 365) × Days_in_Billing_Cycle and verify rounding to cents.\n13. Request same statement with format=PDF; expect 200 and binary/pdf content; ensure access control for non-owner is 403.", + "expectedResult": "Foreign USD transaction correctly applies 3% fee and conversion; rewards accrue with floor rounding per MCC; WebSocket delivers authenticated event; CSRF missing blocks POST; invalid date range rejected; pagination and filters work; rate-limit triggers 429 with mfa_required; statement totals and interest/grace logic are accurate; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/statements/{statement_id}, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "POST, GET, WebSocket", + "endpointGroup": "Transactions, Billing, Realtime", + "workflow": "Transaction Processing, Billing & Rewards, Realtime Feed", + "businessRuleIds": "REQ-006, REQ-009, REQ-010, REQ-012, REQ-013, REQ-015", + "calculationFormula": "REQ-006: Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03. REQ-012/REQ-013: Travel points = floor(amount_cad × 3), Others = floor(amount_cad × 1). REQ-009: Interest = (ADB × APR / 365) × Days_in_Billing_Cycle.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for rate-limit recovery, false for baseline", + "rateLimitBucket": "transactions >10 in 60 min => 429 with mfa_required true", + "inputFields": "account_id,transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description,from_date,to_date,page,per_page,format", + "validationRules": "transaction_amount > 0.00, mcc_code 4 digits, exchange_rate required if currency != CAD (Decimal(8,6)), per_page <= 100, to_date >= from_date, WebSocket requires JWT", + "errorCodesCovered": "INVALID_AMOUNT, CARD_INACTIVE, INSUFFICIENT_FUNDS, FREQ_EXCEEDED, INVALID_DATE_RANGE, FORBIDDEN, CSRF_MISSING", + "stateTransitions": "Realtime subscription: unauthenticated->authenticated; Rate-limit: normal->limited (requires MFA)", + "alertTypesCovered": "ASSUMPTION: STATEMENT_READY notification visible in portal UI after statement generation", + "dataMaskingChecks": "Transaction feed and API return masked PAN only; no PII beyond necessary merchant and masked card info", + "auditTrailChecks": "ASSUMPTION: Transaction approvals logged with user_id/session_id/ip", + "piiFields": "None beyond account_id association; merchant data non-PII", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII minimal", + "assumptions": "ASSUMPTION: Rewards accrue on CAD-posted amounts; ASSUMPTION: Retry-After header included on 429; ASSUMPTION: Latest statement_id is discoverable via statements list.", + "testData": "mcc_code 3000 Travel, mcc_code 5999 Other, exchange_rate 1.250000, amounts 100.00 USD and 10.99 CAD", + "cleanupSteps": "None required; leave transactions for statement validation", + "dependencies": "Account has sufficient credit; Statement generation schedule known for test environment" + }, + { + "type": "functional", + "title": "Essential Service Over-Limit Buffer, Insufficient Funds, Notifications Webhook Idempotency, Owner-Only Access", + "description": "Validates 5% over-limit buffer for essential services, proper denial beyond buffer, notification webhook delivery and idempotency, in-app notification rendering, CSRF enforcement, WebSocket event, and owner-only transaction access.", + "testId": "TC-TXN-OVERLIMIT-003", + "testDescription": "Exercises REQ for essential service over-limit approval and ensures alerts are sent once via idempotency; confirms insufficient funds beyond 5% buffer, feed event, and access controls.", + "prerequisites": "Active card with available_credit set to $100.00; authenticated session with CSRF token; real-time socket connected; notification engine can call webhook.", + "stepsToPerform": "1. Verify account summary shows available_credit approximately $100.00 and account_status Active.\n2. POST /v2/accounts/{account_id}/transactions with X-CSRF-Token for an essential service merchant (ASSUMPTION: MCC 4900 Utilities) amount $104.50 CAD; expect use of 5% buffer.\n3. Confirm 200 approval with over_limit_flag true, updated available_credit reflecting buffer usage, and ISO 8583 approval mapping 00.\n4. Ensure WebSocket event received shows over_limit_flag true; UI displays over-limit banner; no PII exposure.\n5. Trigger internal notifications by POST /v2/notifications/webhook with alert_type OVER_LIMIT, channel IN_APP, severity WARNING, valid idempotency_key; expect 200 with notification_id and delivered_at.\n6. Re-send the exact same webhook payload and idempotency_key; expect 409 DUPLICATE_NOTIFICATION and that the portal UI shows only one alert (idempotency effect).\n7. Attempt another essential service transaction for $106.00 with same available_credit baseline; expect 402 INSUFFICIENT_FUNDS (exceeds 5% buffer), available_credit returned in error body.\n8. Attempt to create a transaction without X-CSRF-Token; expect 403 CSRF_MISSING; retry with CSRF to confirm success path still works for valid amounts.\n9. GET /v2/accounts/{different_account_id}/transactions as this user; expect 403 FORBIDDEN owner-only access enforcement.\n10. Validate notification message_body does not include unmasked PAN or sensitive PII; ensure timestamps are ISO 8601 UTC.\n11. Review server logs or audit (ASSUMPTION) to confirm notification delivery and transaction approvals logged with user_id/session_id/ip_address.", + "expectedResult": "Transaction within 5% buffer is approved with over_limit_flag; beyond buffer is rejected; CSRF is required; notifications webhook delivers once and rejects duplicates; in-app alert renders once; WebSocket event reflects over-limit; owner-only access enforced; PII remains masked.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/notifications/webhook, /v2/accounts/{account_id}/transactions (GET)", + "httpMethod": "POST, POST, GET", + "endpointGroup": "Transactions, Notifications", + "workflow": "Transaction Processing, Notifications & Alerts", + "businessRuleIds": "REQ-006 buffer behavior (domain rule), NFR-05 CSRF", + "calculationFormula": "Over-limit allowed up to 5% of available_credit for essential service MCC (ASSUMPTION: business rule); no foreign fee in CAD case.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard bucket", + "inputFields": "account_id,transaction_amount,merchant_name,merchant_id,mcc_code,transaction_type,alert_type,channel,severity,idempotency_key,message_body", + "validationRules": "Essential MCC eligible for 5% buffer, CSRF required on POST, idempotency_key UUID v4 unique per alert, message_body <= 500 chars", + "errorCodesCovered": "INSUFFICIENT_FUNDS, CSRF_MISSING, FORBIDDEN, DUPLICATE_NOTIFICATION", + "stateTransitions": "Credit available -> over-limit used; Notification state -> delivered -> duplicate ignored", + "alertTypesCovered": "OVER_LIMIT", + "dataMaskingChecks": "Webhook and UI messages do not display PAN; if card referenced, show **** **** **** 1234 only", + "auditTrailChecks": "ASSUMPTION: Notification and transaction approvals logged immutably with user/session/ip", + "piiFields": "None in webhook beyond account_id; ensure masking rules", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII Masking", + "assumptions": "ASSUMPTION: Essential service MCCs include 4900 Utilities; ASSUMPTION: Over-limit buffer precisely 5% over available_credit; ASSUMPTION: CSRF error 403.", + "testData": "available_credit $100.00, essential MCC 4900, amounts $104.50 (approve) and $106.00 (decline), idempotency_key 7b9c8b1e-2f3c-4a0d-a7f1-7d0a8c9b1234", + "cleanupSteps": "Clear in-app notifications after validation; restore available_credit via test harness if needed", + "dependencies": "Notification engine reachable; WebSocket service operational" + }, + { + "type": "functional", + "title": "Card Controls and Loss/Stolen: Freeze/Unfreeze with OTP & CSRF, Blocked Irreversibility, PIN Restrictions, Invalid Transitions", + "description": "Validates card status transitions (Active <-> Frozen), OTP validation and expiry, CSRF enforcement, reporting card lost transitioning to Blocked irreversibly, inability to set PIN on Blocked, and invalid transition handling with audit logging.", + "testId": "TC-CARD-CTRL-004", + "testDescription": "Ensures REQ-007 controls and REQ-008 PIN rules: proper OTP gating, CSRF checks, audit entries, and error handling for invalid or blocked transitions.", + "prerequisites": "Active card_id owned by user@example.com; valid authenticated session, CSRF token, and OTP delivery channel.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm account_status Active and card linked.\n2. PATCH /v2/cards/{card_id}/status to Frozen with confirm_otp valid, reason 'Travel'; include X-CSRF-Token; expect 200 new_status Frozen.\n3. Verify audit trail (ASSUMPTION: admin/audit endpoint) logs user_id, session_id, ip_address, timestamp_utc for status change.\n4. Attempt a PURCHASE via POST /v2/accounts/{account_id}/transactions; expect 403 CARD_INACTIVE with card_status Frozen.\n5. PATCH /v2/cards/{card_id}/status back to Active with valid OTP and CSRF; expect 200 new_status Active; attempt with wrong/expired OTP returns 401 OTP_FAILED (negative) and attempts_remaining decremented.\n6. POST /v2/cards/{card_id}/report-lost with loss_type LOST, optional last_known_use timestamp; include X-CSRF-Token; expect 200 with blocked_card_id, new_card_eta, case_number; account reflects Blocked card.\n7. Attempt PATCH /v2/cards/{card_id}/status to Active or Frozen after Blocked; expect 400 INVALID_TRANSITION (ASSUMPTION) due to irreversibility of Blocked state.\n8. Attempt PUT /v2/cards/{card_id}/pin with valid session_otp while Blocked; expect 403 CARD_BLOCKED; confirm no PIN update.\n9. Attempt to report lost again; expect 409 ALREADY_BLOCKED; verify idempotent behaviors do not create duplicate cases.\n10. Retry PATCH /v2/cards/{card_id}/status without X-CSRF-Token; expect 403 CSRF_MISSING; confirm no status change applied.\n11. Verify UI and API always display masked PAN only; scan DOM for absence of full PAN and sensitive data.", + "expectedResult": "Card Frozen with valid OTP and CSRF; transactions fail while Frozen; Unfreeze succeeds with OTP; reporting lost moves card to Blocked irreversibly; PIN setting disallowed when Blocked; invalid transitions and missing CSRF produce appropriate errors; audit entries recorded; PAN always masked.", + "apiPath": "/v2/cards/{card_id}/status, /v2/cards/{card_id}/report-lost, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions", + "httpMethod": "PATCH, POST, PUT, GET, POST", + "endpointGroup": "Card Management, Accounts, Transactions", + "workflow": "Card Status Control, Loss/Stolen Handling", + "businessRuleIds": "REQ-007, REQ-008, REQ-014, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for OTP actions", + "rateLimitBucket": "standard", + "inputFields": "card_id,status,reason,confirm_otp,loss_type,last_known_use,new_pin,confirm_pin,session_otp", + "validationRules": "status in {Active,Frozen} only, OTP 6 digits valid and unexpired, Blocked state is irreversible, PIN exactly 4 digits and matching", + "errorCodesCovered": "INVALID_TRANSITION, OTP_FAILED, CARD_INACTIVE, CARD_BLOCKED, ALREADY_BLOCKED, CSRF_MISSING", + "stateTransitions": "Active <-> Frozen; Active/Frozen -> Blocked (irreversible)", + "alertTypesCovered": "ASSUMPTION: PIN_LOCKED or FRAUD_FLAG may be generated by backend if needed; not exercised here", + "dataMaskingChecks": "Masked PAN **** **** **** 1234 only; no full PAN in DOM", + "auditTrailChecks": "Status changes logged per NFR-04 with user_id/session_id/ip_address/timestamp_utc", + "piiFields": "None beyond card ownership linkage", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF, Audit Trail", + "assumptions": "ASSUMPTION: Audit verification available via internal endpoint; ASSUMPTION: INVALID_TRANSITION returns 400", + "testData": "OTP valid code 123456 for success, invalid code 000000 for failure", + "cleanupSteps": "None; replacement card issuance handled by operations", + "dependencies": "OTP delivery; audit log access for verification" + }, + { + "type": "functional", + "title": "Payments, Late Fee and Interest, Grace Period, Bank Account Validation, Right to Rescind Window, CSRF and Token Behaviors", + "description": "Validates payment rules (minimums, bank account linkage, scheduling), late fee trigger after due_date+2 days, interest/grace calculations across statements, right-to-rescind within 14 days, CSRF enforcement, and refresh token reuse detection in a financial context.", + "testId": "TC-BILL-PAY-005", + "testDescription": "Covers REQ-011 late fee, REQ-009 interest ADB, REQ-010 grace period, payment validations, REQ-016 rescind window, CSRF and JWT refresh reuse security.", + "prerequisites": "Active account with an open statement and a linked bank_account_id; authenticated session with CSRF; knowledge of due_date and minimum_payment_due.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) to capture due_date, minimum_payment_due, total_balance, prev_statement_balance_paid_in_full flag.\n2. Advance system clock or use test data to be exactly due_date + 3 days; GET statement again; expect late_fee = $35.00 applied per REQ-011 (UTC handling; weekends ignored per ASSUMPTION).\n3. Attempt POST /v2/accounts/{account_id}/payments with payment_type MINIMUM and payment_amount less than minimum_payment_due; include X-CSRF-Token; expect 400 BELOW_MINIMUM with minimum_payment_due echoed.\n4. Attempt POST payment with invalid or inactive bank_account_id; expect 422 INVALID_BANK_ACCOUNT; no change to balance.\n5. Submit valid immediate POST payment with payment_type STATEMENT_BALANCE or FULL_BALANCE using active bank_account_id; expect 200 payment_id, scheduled_date (today), new_balance_estimate near $0 for FULL_BALANCE.\n6. Attempt to schedule a payment with scheduled_date in the past; expect 400 INVALID_SCHEDULED_DATE (ASSUMPTION); then resubmit with a future date (tomorrow) and receive 200 queued response.\n7. After cycle close, GET next statement; compute interest using REQ-009 Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; if previous paid in full before grace window, verify REQ-010 grace applies (no interest); else validate interest amount to cents; verify sum(transaction_amount[]) == total_spend within ±$0.01 (REQ-015).\n8. Perform POST /v2/auth/token/refresh to rotate tokens; then attempt another refresh using the already-used refresh_token; expect 401 TOKEN_INVALID; confirm new refresh works.\n9. Attempt POST /v2/accounts/{account_id}/payments without X-CSRF-Token; expect 403 CSRF_MISSING; retry with CSRF and succeed.\n10. Exercise REQ-016 Right to Rescind: within 14 days from issuance, call DELETE /v2/accounts/{id} with X-CSRF-Token; expect 200 success with audit trail entry and irreversible closure; attempt to access summary after delete returns 403/404.\n11. After day 15 (ASSUMPTION: time travel), call DELETE /v2/accounts/{id}; expect 403 RESCIND_WINDOW_CLOSED and no change to account; verify audit log records attempted rescind denial.", + "expectedResult": "Late fee $35 applies at due_date+3 days; payments enforce minimums and bank account linkage; scheduled_date must be future; interest and grace calculations are correct and statement totals reconcile; CSRF enforced on payments and DELETE; refresh token reuse is rejected; rescind allowed only within 14 days and denied after; audit records present.", + "apiPath": "/v2/accounts/{account_id}/statements/{statement_id}, /v2/accounts/{account_id}/payments, /v2/auth/token/refresh, /v2/accounts/{id}", + "httpMethod": "GET, POST, POST, DELETE", + "endpointGroup": "Billing, Payments, Auth, Account Lifecycle", + "workflow": "Billing & Financial Logic, Payments, Right to Rescind", + "businessRuleIds": "REQ-009, REQ-010, REQ-011, REQ-015, REQ-016, NFR-05", + "calculationFormula": "REQ-009: Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; REQ-011: late_fee = $35 if payment_received_date > due_date + 2 days; REQ-010: Grace period 21 days when prev_statement_balance_paid_in_full = true.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "payments standard", + "inputFields": "payment_amount,payment_type,bank_account_id,scheduled_date,account_id,statement_id", + "validationRules": "payment_amount min $1.00 and >= minimum_payment_due when type MINIMUM, max total_balance, bank_account_id must be active, scheduled_date future only, DELETE allowed within 14 days post-issuance", + "errorCodesCovered": "BELOW_MINIMUM, INVALID_BANK_ACCOUNT, TOKEN_INVALID, CSRF_MISSING, RESCIND_WINDOW_CLOSED", + "stateTransitions": "Account: Active->Closed (rescinded within window) or remains Active if window closed; Statement cycle progression", + "alertTypesCovered": "LATE_PAYMENT (ASSUMPTION via webhook), STATEMENT_READY", + "dataMaskingChecks": "No PAN exposure in payment confirmations; masked details only", + "auditTrailChecks": "Payment attempts and rescind actions logged with user_id/session_id/ip_address/time; credit_limit change audit not in scope here", + "piiFields": "bank_account_id association (non-PII), account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security, CSRF", + "assumptions": "ASSUMPTION: INVALID_SCHEDULED_DATE error on past scheduled_date; ASSUMPTION: RESCIND_WINDOW_CLOSED 403 after day 14; ASSUMPTION: Weekends/holidays do not alter late fee rule.", + "testData": "minimum_payment_due $50.00, total_balance $500.00, bank_account_id e.g. 11111111-2222-3333-4444-555555555555 (active), invalid bank_account_id e.g. 00000000-0000-0000-0000-000000000000", + "cleanupSteps": "If account was rescinded, create a new test account for future tests; clear scheduled payments in test env", + "dependencies": "Statement generation cadence known; APR and ADB available for calculation" + }, + { + "type": "functional", + "title": "Authentication Hardening: Lockout, Rate Limit, Remember Me TTL, Device Recognition, Logout Invalidation", + "description": "Validates login failure handling, account lockout after 5 failures, IP rate limiting (10 req/min), remember_me TTL extension, device recognition, secure cookie flags, and logout invalidating refresh token.", + "testId": "TC-AUTH-LOCKOUT-006", + "testDescription": "Focuses on negative and boundary auth behaviors with regulatory security checks: lockout vs rate limit semantics, secure cookie attributes, and refresh invalidation on logout.", + "prerequisites": "User account exists and email verified for user@example.com; MFA enabled with valid TOTP; known password; test IP can be controlled; browser over TLS 1.3.", + "stepsToPerform": "1. From the same client IP, call POST /v2/auth/login with email user@example.com and an incorrect password four times within 60 seconds.\n2. Verify each of the four attempts returns 401 INVALID_CREDENTIALS and capture any rate-limit headers (ASSUMPTION: X-RateLimit-Remaining present).\n3. Attempt a 5th invalid login within the same window; expect 403 ACCOUNT_LOCKED with unlock_at timestamp returned.\n4. Immediately attempt login with the correct password and valid 6-digit TOTP while account is locked; expect 403 ACCOUNT_LOCKED persists.\n5. Wait until unlock_at + 1 minute; attempt login again with correct password, valid TOTP, device_id set (UUID v4), and remember_me=true; expect 200 with access_token and refresh_token in HttpOnly, Secure, SameSite=Strict cookies.\n6. Validate cookie flags and that no tokens are present in window.localStorage or window.sessionStorage; confirm tokens are cookies only.\n7. Verify remember_me extended TTL: inspect refresh cookie expiry or response (ASSUMPTION: refresh_expires_in present) equals ~30 days vs default.\n8. From the same IP, send 10 additional login requests in the current minute (successful or attempted); trigger an 11th login attempt; expect 429 RATE_LIMITED with retry_after header and no tokens issued.\n9. Logout via POST /v2/auth/logout including a valid X-CSRF-Token; expect 200 and that access/refresh cookies are cleared (expired) and SameSite=Strict remains enforced.\n10. Attempt POST /v2/auth/token/refresh using the pre-logout refresh_token; expect 401 TOKEN_INVALID due to rotation/invalidation on logout.\n11. Attempt GET /v2/accounts/{account_id}/summary without fresh login; expect 401 (unauthenticated) and no data exposure.\n12. Login again with the same device_id; ASSUMPTION: response includes device_recognized=true or audit log records trusted device recognition; verify MFA still required as per policy (no bypass).", + "expectedResult": "Account locks after 5 failed attempts with unlock_at; rate limiting returns 429 on 11th request/min with retry_after; remember_me extends refresh TTL to 30 days; cookies are HttpOnly, Secure, SameSite=Strict; logout clears cookies and invalidates refresh token; accessing protected resources without auth returns 401; device recognition is recorded without reducing security.", + "apiPath": "/v2/auth/login, /v2/auth/logout, /v2/auth/token/refresh, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, POST, GET", + "endpointGroup": "Auth, Accounts", + "workflow": "Authentication & Session Management", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "false for login, true for summary", + "csrfRequired": "true for logout only", + "mfaRequired": "true for successful login", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "email,password,mfa_code,device_id,remember_me", + "validationRules": "Password complexity enforced at registration; login lock after 5 failures; rate limit 10/min per IP; tokens in Secure, HttpOnly, SameSite=Strict cookies only", + "errorCodesCovered": "INVALID_CREDENTIALS, ACCOUNT_LOCKED, RATE_LIMITED, TOKEN_INVALID, UNAUTHORIZED", + "stateTransitions": "Auth: unlocked->locked->unlocked; Session: logged_in->logged_out", + "dataMaskingChecks": "No tokens in localStorage/sessionStorage; cookies only; no PII in error messages", + "auditTrailChecks": "ASSUMPTION: Login attempts, lock, unlock, and logout recorded with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: refresh_expires_in or cookie expiry visible; ASSUMPTION: device_recognized flag or audit evidence available; ASSUMPTION: X-RateLimit-Remaining and retry_after headers present.", + "testData": "email user@example.com, device_id 8c4a8f1e-3c1d-4f9d-9a2c-1b2c3d4e5f60", + "cleanupSteps": "Ensure account is unlocked post-test; clear cookies/session; reset rate-limit counters if needed", + "dependencies": "MFA seed provisioned; CSRF token retrieval endpoint available" + }, + { + "type": "functional", + "title": "Credit Application: Step Order Enforcement, Session Token Validation, PENDING and DECLINED Decisions", + "description": "Verifies sequential step enforcement, X-App-Session header validation, decision thresholds for PENDING and DECLINED, marketing_opt_in default, and signature validation.", + "testId": "TC-APP-ORDER-007", + "testDescription": "Covers multi-path outcomes and error handling: invalid step order, expired/invalid session_token, e_signature missing, PENDING (FICO 600-680), DECLINED (<600), and duplicate application rule.", + "prerequisites": "Two verified users exist: userA@example.com and userB@example.com; authenticated sessions with CSRF tokens; products catalog contains a valid card_product_id.", + "stepsToPerform": "1. For userA@example.com, call POST /v2/applications/start with valid Step 1 data (full_legal_name, email matching authenticated user, phone +14165550111, valid Canadian address, id_type PASSPORT, id_number XABC12345); capture application_id and session_token.\n2. Attempt to skip Step 2 by calling POST /v2/applications/{application_id}/submit with card_product_id AEGIS_GOLD and a valid e_signature; expect 400 INVALID_STEP_ORDER (ASSUMPTION) and no decision produced.\n3. Call POST /v2/applications/{application_id}/financials with an intentionally invalid X-App-Session header (expired or tampered token); expect 401 SESSION_EXPIRED and confirm no fico_pull_id returned.\n4. Reattempt Step 2 with the correct session_token but omit employer_name while employment_status=EMPLOYED; expect 400 with field=employer_name; correct the payload by adding employer_name and sin_consent=true; expect 200 with status PENDING_REVIEW and fico_pull_id.\n5. Configure the bureau stub for userA to return FICO=620 (ASSUMPTION: test harness); call POST /v2/applications/{application_id}/submit with valid e_signature Base64 and marketing_opt_in omitted; expect 200 with decision PENDING, review_eta_hours present, and marketing_opt_in default false.\n6. Re-submit Step 3 immediately; expect idempotent behavior (200 returning same PENDING) or 409 ALREADY_SUBMITTED (ASSUMPTION); verify no duplicate application records created.\n7. For userB@example.com, call POST /v2/applications/start with valid Step 1 data; capture application_id and session_token.\n8. Call POST /v2/applications/{application_id}/financials with valid data and sin_consent=true; set bureau stub to FICO=580 (ASSUMPTION) to trigger DECLINED.\n9. Call POST /v2/applications/{application_id}/submit with malformed e_signature (not Base64 or missing); expect 400 SIGNATURE_REQUIRED; correct e_signature and resubmit; expect 200 with decision DECLINED and reason_code populated.\n10. While userB has a DECLINED outcome, attempt to start a new application again via POST /v2/applications/start; if business rule disallows concurrent active applications, expect 409 DUPLICATE_APPLICATION until prior is closed (ASSUMPTION); otherwise expect 201 for a new application and document observed behavior.\n11. Attempt POST /v2/applications/{application_id}/submit without the X-App-Session header; expect 401 SESSION_EXPIRED per requirement that Steps 2 and 3 must carry session_token.", + "expectedResult": "System enforces sequential steps; invalid session_token yields 401; e_signature required; decision outcomes adhere to thresholds (620=PENDING, 580=DECLINED with reason_code); marketing_opt_in defaults false when omitted; resubmission is idempotent; duplicate application rule enforced per spec.", + "apiPath": "/v2/applications/start, /v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002", + "calculationFormula": "Decision thresholds: FICO > 680 APPROVED; 600-680 PENDING; <600 DECLINED.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for account session if policy requires, not per endpoint", + "rateLimitBucket": "applications standard", + "inputFields": "full_legal_name,email,phone_number,residential_address,id_type,id_number,employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature", + "validationRules": "email must match authenticated user, X-App-Session required for Steps 2 and 3, employer_name required if EMPLOYED, e_signature must be Base64 encoded", + "errorCodesCovered": "INVALID_STEP_ORDER, SESSION_EXPIRED, SIGNATURE_REQUIRED, ALREADY_SUBMITTED, DUPLICATE_APPLICATION", + "stateTransitions": "Application: NEW->STEP1->STEP2(PENDING_REVIEW)->SUBMITTED->PENDING or DECLINED", + "dataMaskingChecks": "No full PAN in any application responses; no tokens exposed in UI storage", + "auditTrailChecks": "ASSUMPTION: Application creation and decision recorded with user_id, session_id, ip_address", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PII, Security", + "assumptions": "ASSUMPTION: Error INVALID_STEP_ORDER is returned when Step 3 is called before Step 2; ASSUMPTION: Bureau stub allows fixed FICO for tests; ASSUMPTION: Step 3 requires X-App-Session header though not shown in table.", + "testData": "userA@example.com FICO 620 (PENDING); userB@example.com FICO 580 (DECLINED); card_product_id AEGIS_GOLD", + "cleanupSteps": "Close or archive test applications via test harness; log out both users", + "dependencies": "Products catalog available; credit bureau stub configurable" + }, + { + "type": "functional", + "title": "Rewards Accrual Boundary Values and MCC Classification with Floor Rounding", + "description": "Validates REQ-012/REQ-013 rewards rules across MCC categories and boundary amounts (.99, .01, 1.00), ensuring floor-only rounding and correct statement totals (REQ-015).", + "testId": "TC-REWARDS-BV-008", + "testDescription": "Creates a targeted set of CAD purchases to test Travel vs Other MCC mapping and floor rounding; verifies points at account summary and statement, and rejects invalid MCC.", + "prerequisites": "Active account_id owned by user@example.com with sufficient available_credit; authenticated session with CSRF; points balance known (capture baseline).", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary with include_rewards=true to capture baseline points_balance P0 and confirm account_status Active.\n2. POST /v2/accounts/{account_id}/transactions with mcc_code 3000 (Travel), amount 1.00 CAD, merchant_name 'TravelOne', merchant_id 'T001'; expect 200 approval.\n3. POST /v2/accounts/{account_id}/transactions with mcc_code 3000 (Travel), amount 0.99 CAD, merchant_name 'TravelTwo', merchant_id 'T002'; expect 200 approval.\n4. POST /v2/accounts/{account_id}/transactions with mcc_code 5999 (Other), amount 1.00 CAD, merchant_name 'ShopOne', merchant_id 'S001'; expect 200 approval.\n5. POST /v2/accounts/{account_id}/transactions with mcc_code 5999 (Other), amount 0.01 CAD, merchant_name 'ShopTwo', merchant_id 'S002'; expect 200 approval.\n6. Attempt POST /v2/accounts/{account_id}/transactions with an invalid mcc_code '123' (3 digits) and amount 1.00; expect 400 validation error or 422 (implementation-specific) and no transaction created.\n7. GET /v2/accounts/{account_id}/transactions with category=PURCHASE, page=1, per_page=25 to confirm the four valid purchases are present with correct amounts and MCCs.\n8. Compute expected rewards per REQ-012/REQ-013: Travel points = floor(1.00*3)+floor(0.99*3)=3+2=5; Other points = floor(1.00*1)+floor(0.01*1)=1+0=1; total expected increment Δ=6.\n9. GET /v2/accounts/{account_id}/summary with include_rewards=true; verify points_balance == P0 + 6 (or that the next statement reflects +6 if accrual is statement-based; document observed accrual timing).\n10. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) in JSON; verify rewards_earned includes the +6 points from these purchases and that sum(transaction_amount[]) equals total_spend within ±$0.01 (REQ-015).", + "expectedResult": "Rewards classify correctly by MCC (3000=Travel, 5999=Other); floor rounding applied so 0.99 Travel -> 2 points and 0.01 Other -> 0 points; total rewards increment equals 6; invalid MCC is rejected; statement totals reconcile within tolerance.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "POST, GET, GET", + "endpointGroup": "Transactions, Accounts, Billing", + "workflow": "Billing, Rewards & Financial Logic", + "businessRuleIds": "REQ-012, REQ-013, REQ-015", + "calculationFormula": "Travel points = floor(amount × 3); Other points = floor(amount × 1); totals must floor each line item and sum.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "transaction_amount,mcc_code,merchant_name,merchant_id,category,page,per_page,include_rewards", + "validationRules": "mcc_code must be 4 digits; transaction_amount > 0; per_page <=100", + "errorCodesCovered": "INVALID_AMOUNT, validation error for mcc_code, FORBIDDEN (if access attempt to another account)", + "stateTransitions": "Points balance P0 -> P0+Δ after accrual", + "dataMaskingChecks": "No PAN visible; masked only **** **** **** 1234 in any UI feed related to transactions", + "auditTrailChecks": "ASSUMPTION: Transaction creation logged with user_id/session_id/ip_address", + "piiFields": "None beyond account ownership relation", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1", + "assumptions": "ASSUMPTION: Rewards accrue either realtime or at statement close; test accepts either with documentation; statement_id for latest cycle is discoverable.", + "testData": "MCC Travel=3000, Other=5999; amounts: 1.00, 0.99, 1.00, 0.01", + "cleanupSteps": "None; transactions remain for statement verification", + "dependencies": "Sufficient available_credit; statement generation schedule known" + }, + { + "type": "functional", + "title": "WebSocket Live Feed Resilience: JWT Expiry Handling, Reconnect, Unauthorized Subscription", + "description": "Validates authenticated subscription, JWT expiry during session, reconnect with refreshed token, forbidden access to other accounts, and secure transport use.", + "testId": "TC-WS-RESILIENCE-009", + "testDescription": "Exercises realtime feed auth and reliability: token expiration mid-connection, reconnection flow, subscription scoping, and absence of PII in events.", + "prerequisites": "Active account_id; valid access/refresh tokens; ability to create transactions; browser supports WebSocket over TLS 1.3.", + "stepsToPerform": "1. Connect to wss://realtime.aegiscard.com/v2/stream using Authorization: Bearer ; subscribe to topic account:{account_id}:transactions; expect subscription ack.\n2. Trigger a CAD PURCHASE via POST /v2/accounts/{account_id}/transactions; expect a WebSocket event with transaction_id, amounts, masked PAN, and no PII leakage (no full PAN, SSN, or tokens).\n3. Keep the connection open until the access token expires (15 minutes); send a ping or attempt a no-op; expect the server to close the connection or send 401/unauthorized event due to expired JWT (ASSUMPTION per implementation).\n4. Call POST /v2/auth/token/refresh to obtain a new access_token (refresh rotates); verify old refresh cannot be reused.\n5. Reconnect the WebSocket with the new access_token; resubscribe to account:{account_id}:transactions; expect ack and healthy heartbeats (ASSUMPTION: heartbeat/ping supported).\n6. Attempt to subscribe to account:{other_account_id}:transactions; expect immediate 403 FORBIDDEN or subscription error with no events delivered.\n7. Trigger another PURCHASE on own account; verify a single event is received (no duplicates) and payload integrity maintained.\n8. Simulate a transient network drop by forcibly closing the socket; within 10 seconds, reconnect and resubscribe; verify subsequent events continue streaming without duplication.\n9. Attempt to pass JWT via query string instead of Authorization header; expect connection rejected or downgraded privileges; reconnect properly using Authorization header and confirm success.\n10. Verify all frames are over TLS 1.3; confirm no downgrade; ensure no sensitive tokens are echoed in any server message.", + "expectedResult": "Authenticated subscription works; token expiry causes disconnect or 401; refresh and reconnect restore the stream; attempts to subscribe to other accounts are forbidden; events include masked PAN only; insecure token conveyance (query string) is rejected; connection remains on TLS 1.3.", + "apiPath": "wss://realtime.aegiscard.com/v2/stream, /v2/accounts/{account_id}/transactions, /v2/auth/token/refresh", + "httpMethod": "WebSocket, POST, POST", + "endpointGroup": "Realtime, Transactions, Auth", + "workflow": "Notifications & Realtime Feed", + "businessRuleIds": "REQ-014 for masking, NFR-01 TLS", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST transaction, false for WebSocket connect", + "mfaRequired": "false for socket after login", + "rateLimitBucket": "transactions standard; socket connection limits not specified", + "inputFields": "Authorization header, subscription topic account:{account_id}:transactions", + "validationRules": "WebSocket requires JWT; owner-only topic subscription; events must not include PII", + "errorCodesCovered": "FORBIDDEN (topic), TOKEN_INVALID on refresh reuse, UNAUTHORIZED on expired JWT", + "stateTransitions": "Socket: connected->expired->reconnected", + "dataMaskingChecks": "Event payloads use **** **** **** 1234; no tokens or PII in messages", + "auditTrailChecks": "ASSUMPTION: Connections and subscription attempts logged with user_id/ip", + "piiFields": "None", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Server closes socket or emits 401 on expired JWT; ASSUMPTION: heartbeat frames available; ASSUMPTION: JWT in query string is unsupported.", + "testData": "other_account_id different from owner", + "cleanupSteps": "Close WebSocket; logout; revoke refresh token", + "dependencies": "Realtime service operational; ability to create transactions in test account" + }, + { + "type": "functional", + "title": "Access Control and CSRF Enforcement Matrix: 401 vs 403, SameSite=Strict Cross-Site Protection", + "description": "Validates unauthenticated vs unauthorized responses across endpoints, CSRF enforcement on state-changing calls, and SameSite=Strict preventing cross-site cookie send.", + "testId": "TC-ACCESS-CSRF-010", + "testDescription": "Covers precise 401/403 mappings, CSRF presence and validity, cross-origin submission blocked by SameSite cookies, and secure cookie attributes.", + "prerequisites": "Two accounts exist: account_id_owned and account_id_other; user logged out initially; browser devtools access; CSRF issuance endpoint available.", + "stepsToPerform": "1. While logged out, call GET /v2/accounts/{account_id_owned}/summary without Authorization cookie; expect 401 (unauthenticated) and no body data.\n2. Login to portal; confirm access and obtain CSRF token via GET /v2/auth/csrf (ASSUMPTION) or response meta.\n3. GET /v2/accounts/{account_id_owned}/summary; expect 200 with balances and owner-only access confirmed.\n4. GET /v2/accounts/{account_id_other}/summary using current token; expect 403 FORBIDDEN (unauthorized) and no data exposure.\n5. Attempt POST /v2/accounts/{account_id_owned}/transactions with valid payload but without X-CSRF-Token; expect 403 CSRF_MISSING and no transaction created.\n6. Retry the same POST including X-CSRF-Token; expect 200 approval and transaction_id present.\n7. From a different origin (e.g., https://evil.example), attempt to submit a cross-site HTML form POST to /v2/accounts/{account_id_owned}/transactions; verify SameSite=Strict prevents auth cookies from being sent, leading to 401 UNAUTHORIZED and no side effects (ASSUMPTION: CORS blocks or cookies omitted).\n8. Inspect cookies in the browser; verify Secure, HttpOnly, SameSite=Strict flags set; ensure no tokens in localStorage/sessionStorage.\n9. Logout via POST /v2/auth/logout with current CSRF; obtain a new login session and fetch a new CSRF token; attempt POST /v2/accounts/{account_id_owned}/transactions using the old CSRF from the previous session; expect 403 CSRF_INVALID or CSRF_MISSING (implementation-specific) and no transaction created.\n10. Confirm that error responses never include PII and that rate-limiting headers appear only on applicable responses (e.g., 429) and not on access-control denials.", + "expectedResult": "Unauthenticated access yields 401; unauthorized access to other accounts yields 403; CSRF is required and validated per session; cross-site posts fail due to SameSite=Strict; secure cookie attributes enforced; no PII in errors.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/auth/logout, /v2/auth/csrf", + "httpMethod": "GET, POST, POST, GET", + "endpointGroup": "Accounts, Transactions, Auth", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for owner-only resources", + "csrfRequired": "true for POST/DELETE/PATCH/PUT", + "mfaRequired": "false for this flow", + "rateLimitBucket": "N/A", + "inputFields": "X-CSRF-Token, transaction_amount, merchant_name, merchant_id, mcc_code", + "validationRules": "CSRF required on mutating requests; owner-only access on account resources; SameSite=Strict on cookies", + "errorCodesCovered": "UNAUTHORIZED, FORBIDDEN, CSRF_MISSING, CSRF_INVALID", + "stateTransitions": "Session: logged_out->logged_in->logged_out->logged_in", + "dataMaskingChecks": "No sensitive data in error payloads; PAN never present", + "auditTrailChecks": "ASSUMPTION: Access denials and logout recorded", + "piiFields": "None returned on errors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf exists; ASSUMPTION: CSRF token is session-bound; ASSUMPTION: Cross-origin request lacks credentials due to SameSite=Strict.", + "testData": "account_id_owned vs account_id_other distinct values; CSRF tokens old/new", + "cleanupSteps": "Clear cookies/session; remove any test transactions created", + "dependencies": "CORS and cookie policies configured; CSRF service available" + }, + { + "type": "functional", + "title": "Registration Validation: Email Uniqueness (Case-Insensitive), Password Complexity, DOB >=18 (Leap-Year), Phone E.164, Terms Required", + "description": "Validate registration field rules and error codes including cross-case email uniqueness, password complexity boundaries, age calculation on leap-year birthdays, E.164 phone, ssn_last4 length, and agree_terms enforcement with TLS 1.3.", + "testId": "TC-AUTH-REG-011", + "testDescription": "Ensures POST /v2/auth/register enforces all validations and returns correct status codes and fields; confirms verification email trigger and no sensitive data exposure.", + "prerequisites": "No existing account for user@example.com; network uses TLS 1.3; WAF and API gateway reachable.", + "stepsToPerform": "1. Confirm browser session negotiates TLS 1.3 with https://api.aegiscard.com/v2 (check security info/HSTS) and no mixed-content warnings.\n2. POST /v2/auth/register with agree_terms=false and otherwise valid data (email new_user@example.com, strong password, valid DOB, phone +14165550123, ssn_last4 1234); expect rejection due to terms.\n3. POST /v2/auth/register with agree_terms=true but weak password 'Short1!' (length < 12); expect 422 WEAK_PASSWORD.\n4. POST /v2/auth/register with password strong but date_of_birth set to exactly 17 years, 364 days ago (edge just under 18) including leap-year handling; expect 400 with field=date_of_birth.\n5. POST /v2/auth/register with invalid phone_number '4165550123' (missing +country code); expect 400 with field=phone_number (E.164 required).\n6. POST /v2/auth/register with ssn_last4 '123' (3 digits); expect 400 with field=ssn_last4 exact length rule enforced.\n7. POST /v2/auth/register with valid payload: first_name John, last_name Public, email user@example.com, strong password >=12 with upper/lower/digit/symbol, date_of_birth exactly 18 years ago today (boundary), phone_number +14165550123, ssn_last4 1234, agree_terms true; expect 201 with user_id and verification_token present.\n8. Immediately attempt POST /v2/auth/register again using same email but different case 'USER@EXAMPLE.COM'; expect 409 EMAIL_EXISTS confirming case-insensitive uniqueness.\n9. Validate that server indicates verification workflow triggered (verification_token in response or ASSUMPTION: an outbound email log entry exists); capture token for potential later use.\n10. Inspect browser storage to confirm no JWTs or PII stored in localStorage/sessionStorage; ensure HttpOnly cookie policy (if any set) and that no PAN or sensitive data appears in DOM.", + "expectedResult": "Registration enforces all field rules: terms required, strong password, E.164 phone, ssn_last4 exactly 4 digits, DOB >=18; email uniqueness is case-insensitive; success returns 201 with verification_token; TLS 1.3 in effect; no sensitive data stored in frontend.", + "apiPath": "/v2/auth/register", + "httpMethod": "POST", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-01, Registration Rules", + "calculationFormula": "Age >= 18 computed from date_of_birth considering leap years; email uniqueness normalized case-insensitively.", + "rolesCovered": "Cardholder", + "authRequired": "false", + "csrfRequired": "false", + "mfaRequired": "false", + "rateLimitBucket": "login not exercised; registration standard", + "inputFields": "first_name,last_name,email,password,date_of_birth,phone_number,ssn_last4,agree_terms", + "validationRules": "Email RFC5322 and unique (case-insensitive), Password >=12 with upper/lower/digit/symbol, DOB >=18 with leap-year handling, Phone E.164, ssn_last4 exactly 4 digits, agree_terms true", + "errorCodesCovered": "WEAK_PASSWORD, EMAIL_EXISTS, 400 field validation errors", + "stateTransitions": "User: none->registered (pending verification)", + "dataMaskingChecks": "No PAN in DOM; no JWTs in localStorage/sessionStorage; PII only in request/response as specified", + "auditTrailChecks": "ASSUMPTION: Registration event logged with user_id/ip/timestamp", + "piiFields": "first_name,last_name,email,date_of_birth,phone_number,ssn_last4", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Verification token or email log available in test environment; ASSUMPTION: Age calculation uses UTC date; ASSUMPTION: WAF/HSTS observable at test time.", + "testData": "email user@example.com, phone +14165550123, ssn_last4 1234, strong password e.g. 'Str0ngPass!2026'", + "cleanupSteps": "None required; optionally delete test user via admin tool (non-prod).", + "dependencies": "Email verification system or stub available for observing verification_token" + }, + { + "type": "functional", + "title": "Refresh Token Rotation Concurrency Across Tabs: Single-Use Enforcement, Session Isolation, CSRF Scope", + "description": "Validate refresh token single-use rotation when two browser tabs attempt refresh; ensure the first succeeds, the second is rejected; verify CSRF token is session-bound and cannot be re-used across tabs; logout invalidates refresh globally; cookie flags enforced.", + "testId": "TC-AUTH-ROTATE-012", + "testDescription": "Simulates multi-tab browser behavior to assert JWT refresh rotation, reuse detection (401), CSRF session binding, and logout invalidation across tabs with Secure, HttpOnly, SameSite=Strict cookies.", + "prerequisites": "Existing verified user user@example.com with MFA; ability to open two tabs (Tab A, Tab B) sharing initial session; CSRF issuance endpoint available.", + "stepsToPerform": "1. In Tab A, POST /v2/auth/login with email user@example.com, correct password, device_id UUIDv4, remember_me=false; complete MFA with valid 6-digit TOTP; expect 200 and cookies set for access_token and refresh_token (Secure, HttpOnly, SameSite=Strict).\n2. Open Tab B (same browser profile), confirm authenticated context via a protected GET /v2/accounts/{account_id}/summary returning 200 (owner-only account).\n3. In Tab A, call POST /v2/auth/token/refresh; expect 200 with new rotated refresh_token (R1-new) and new access_token; old refresh invalidated.\n4. Immediately in Tab B, call POST /v2/auth/token/refresh using the now-stale refresh cookie; expect 401 TOKEN_INVALID; verify Tab B UI forces re-auth and clears tokens.\n5. In Tab A, GET /v2/auth/csrf (ASSUMPTION endpoint) and store X-CSRF-Token CSRF-A; POST /v2/auth/logout with header X-CSRF-Token: CSRF-A; expect 200 and cookies expired in Tab A.\n6. Without logging in, in Tab B attempt POST /v2/auth/logout using CSRF-A; expect 403 CSRF_INVALID or CSRF_MISSING since CSRF tokens are session-bound; no state change.\n7. Log back in on Tab B only (MFA successful); confirm new access and refresh cookies set; GET /v2/auth/csrf to obtain CSRF-B (distinct from CSRF-A).\n8. Attempt POST /v2/auth/token/refresh in Tab B twice in quick succession: first call returns 200 rotated tokens, second call (reusing the first refresh) returns 401 TOKEN_INVALID; observe UI gracefully handles second failure.\n9. Validate cookies across both tabs are HttpOnly, Secure, SameSite=Strict; inspect storage to ensure no tokens exist in localStorage/sessionStorage.\n10. From Tab A (still logged out), try accessing a protected endpoint GET /v2/accounts/{account_id}/summary; confirm 401 UNAUTHORIZED and that re-login restores access.", + "expectedResult": "First refresh succeeds, concurrent/stale refresh attempts return 401 TOKEN_INVALID; CSRF tokens are session-bound and cannot be reused across tabs; logout invalidates tokens globally; cookies carry correct security flags; no tokens reside in web storage.", + "apiPath": "/v2/auth/login, /v2/auth/token/refresh, /v2/auth/csrf, /v2/auth/logout, /v2/accounts/{account_id}/summary", + "httpMethod": "POST, POST, GET, POST, GET", + "endpointGroup": "Auth, Accounts", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for summary, false for login/refresh", + "csrfRequired": "true for logout only", + "mfaRequired": "true for successful login", + "rateLimitBucket": "login 10 req/min per IP (not exceeded in this test)", + "inputFields": "email,password,mfa_code,device_id,X-CSRF-Token", + "validationRules": "Refresh token rotation single-use, CSRF token must match active session, cookies must be Secure, HttpOnly, SameSite=Strict", + "errorCodesCovered": "TOKEN_INVALID, CSRF_INVALID, UNAUTHORIZED", + "stateTransitions": "Session: logged_in(Tab A,B)->Tab A refreshed->Tab B invalid refresh->Tab A logout->Tab B re-login", + "dataMaskingChecks": "No sensitive tokens in frontend storage; no PII in error responses", + "auditTrailChecks": "ASSUMPTION: Login, refresh, logout events logged with user_id/session_id/ip/timestamp", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf exists and CSRF is session-bound; ASSUMPTION: UI clears tokens on 401 refresh failure; ASSUMPTION: SameSite=Strict enforced.", + "testData": "email user@example.com, device_id 550e8400-e29b-41d4-a716-446655440001", + "cleanupSteps": "Logout from all tabs; clear cookies; reset any lockouts if accidentally triggered.", + "dependencies": "MFA seed available; CSRF issuance endpoint available" + }, + { + "type": "functional", + "title": "Transaction Parameter Validation and FX Precision: Required exchange_rate, Decimal(8,6) Boundaries, INVALID_AMOUNT Zero", + "description": "Validate transaction field constraints including amount > 0, required exchange_rate when currency != CAD, precision limits for Decimal(8,6), and foreign fee calculation/rounding correctness for small amounts; confirm pagination page minimum.", + "testId": "TC-TXN-VALID-013", + "testDescription": "Covers negative and boundary validations for POST /v2/accounts/{account_id}/transactions and verifies REQ-006 math for low-value FX with two-decimal rounding; includes listing parameter boundary (page >=1).", + "prerequisites": "Active account_id owned by user@example.com with available_credit >= $50.00; authenticated session with X-CSRF-Token; merchant test IDs available.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm account_status Active and available_credit >= $50.00; capture baseline available_credit.\n2. POST /v2/accounts/{account_id}/transactions with transaction_amount 0.00 CAD, valid merchant fields, mcc_code 5999, transaction_type PURCHASE, include X-CSRF-Token; expect 422 INVALID_AMOUNT.\n3. POST /v2/accounts/{account_id}/transactions with currency_code USD, transaction_amount 5.00, but omit exchange_rate; expect 400 validation error for missing exchange_rate when currency != CAD.\n4. POST /v2/accounts/{account_id}/transactions with currency_code USD, transaction_amount 1.23, exchange_rate 1.3333337 (7 decimal places); expect 400 validation error for exchange_rate precision beyond Decimal(8,6).\n5. POST valid FX transaction: currency_code USD, transaction_amount 1.23, exchange_rate 1.333333 (max precision), mcc_code 3000, merchant_name 'MiniTravel', merchant_id 'MT001', transaction_type PURCHASE, X-CSRF-Token present; expect 200 approval.\n6. Compute expected REQ-006 values: base_cad = 1.23 × 1.333333 = 1.63999959 → round to cents at final amounts only; foreign_fee_amount = base_cad × 0.03 = 0.0491999877 ≈ 0.05; Total_CAD = base_cad × 1.03 = 1.6891993777 ≈ 1.69; verify response itemises foreign_fee_amount 0.05 and Total_CAD impact reflected in available_credit.\n7. POST a non-FX transaction with transaction_type CASH_ADVANCE, transaction_amount 10.00 CAD, mcc_code 6010 (ASSUMPTION valid), merchant_name 'CashPoint', merchant_id 'CA001'; expect 200 approval or domain-appropriate handling; record category for listing.\n8. POST a BALANCE_TRANSFER transaction amount 15.00 CAD, mcc_code 6012 (ASSUMPTION valid), merchant_name 'BalanceXfer', merchant_id 'BT001'; expect 200 approval; record category.\n9. GET /v2/accounts/{account_id}/transactions with page=0 (invalid), per_page=25; expect 400 invalid page (min 1).\n10. GET /v2/accounts/{account_id}/transactions with page=1, per_page=25 and category filters PURCHASE,CASH_ADVANCE in separate calls; verify items returned match submitted types and that owner-only access is enforced (403 if querying another account).", + "expectedResult": "Transactions validate amount > 0; missing exchange_rate for non-CAD rejected; exchange_rate precision > 6 decimals rejected; valid FX applies 3% fee with correct two-decimal rounding; listing page must be >=1; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/transactions (POST), /v2/accounts/{account_id}/transactions (GET)", + "httpMethod": "POST, GET", + "endpointGroup": "Transactions", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006", + "calculationFormula": "REQ-006: Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03; final monetary amounts rounded to two decimals.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard (no frequency limit reached)", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,description,page,per_page,category", + "validationRules": "transaction_amount > 0.00, mcc_code 4 digits, exchange_rate required when currency != CAD with Decimal(8,6), page >=1, per_page <=100", + "errorCodesCovered": "INVALID_AMOUNT, INVALID_DATE_RANGE (for list if date filters used), FORBIDDEN, 400 field validation", + "stateTransitions": "Available credit decreases per approved transaction; list filters reflect transaction types", + "dataMaskingChecks": "Responses must not include full PAN; masked **** **** **** 1234 where applicable", + "auditTrailChecks": "ASSUMPTION: Transaction approvals logged with user_id/session_id/ip", + "piiFields": "None beyond account ownership linkage and merchant descriptors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: mcc_code 6010 and 6012 acceptable for CASH_ADVANCE/BALANCE_TRANSFER; ASSUMPTION: Rounding applied at currency amount presentation, not intermediate multiplications.", + "testData": "USD exchange_rate 1.333333; amounts 1.23 USD, 10.00 CAD, 15.00 CAD; MCCs 3000, 6010, 6012", + "cleanupSteps": "None; retain transactions for later statement validation.", + "dependencies": "Sufficient available_credit; CSRF token present" + }, + { + "type": "functional", + "title": "Statement Calculations Boundary: ADB Interest Rounding, Grace Eligibility Edge, Late Fee UTC +2 Days Threshold, Not Found", + "description": "Validate REQ-009 interest calculation and rounding to cents across cycle lengths and APR, REQ-010 grace period boundary when paid-in-full exactly vs slightly under, REQ-011 late fee at due_date+2 days (UTC) boundary, and 404 for unknown statement_id.", + "testId": "TC-BILL-CALC-014", + "testDescription": "Exercises precise billing edges and time boundaries, ensuring amounts reconcile and errors are correct for missing statements.", + "prerequisites": "Account with known APR (e.g., 19.99%), recent cycles available; ability to set test data for prev_statement_balance_paid_in_full and payment_received_date times; authenticated session; statement_id list accessible.", + "stepsToPerform": "1. Retrieve latest statements list (ASSUMPTION: via a list API or portal) and pick the previous cycle; GET /v2/accounts/{account_id}/statements/{statement_id_prev} to read prev_statement_balance_paid_in_full and due_date.\n2. Configure test data so prev_statement_balance_paid_in_full=true with previous cycle fully paid on time; after current cycle close, GET /v2/accounts/{account_id}/statements/{statement_id_curr}; expect interest_charged=0 per REQ-010.\n3. Re-run with prev_statement_balance_paid_in_full=false by simulating a tiny unpaid remainder of $0.01 last cycle; after current cycle close, GET statement_id_curr2; confirm interest_charged > 0 computed by REQ-009.\n4. Validate REQ-009: using returned adb and cycle days, compute Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; compare to interest_charged rounded to cents; verify tolerance exactly matches rounding rules.\n5. Verify total_spend equals sum(transaction_amount[]) within ±$0.01 tolerance (REQ-015) using the JSON statement payload.\n6. Boundary late fee: set payment_received_date exactly at due_date + 2 days (23:59:59.000 UTC) and re-generate or fetch recalculated statement; expect late_fee=0.\n7. Set payment_received_date to due_date + 2 days + 1 second (00:00:01 UTC next moment); GET updated statement; expect late_fee=$35.00 (REQ-011) regardless of weekend/holiday (ASSUMPTION).\n8. Request the same statement in PDF by adding format=PDF; verify 200 and application/pdf content type; ensure owner-only access (403 when using another account_id).\n9. GET /v2/accounts/{account_id}/statements/{nonexistent_statement_id}; expect 404 NOT_FOUND; no sensitive data in error payload.\n10. Confirm rewards_earned field presence; if present, ensure it follows floor-only rounding for any Travel vs Other transactions recorded (consistency check with REQ-012/REQ-013, not recalculation in this test).", + "expectedResult": "Interest is zero when paid-in-full; otherwise matches REQ-009 formula to cents; late fee applies only after due_date+2 days (UTC) boundary; statement totals reconcile within tolerance; PDF/JSON retrieval works; 404 returned for unknown statement_id; owner-only access enforced.", + "apiPath": "/v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "GET", + "endpointGroup": "Billing", + "workflow": "Billing, Rewards & Financial Logic", + "businessRuleIds": "REQ-009, REQ-010, REQ-011, REQ-015", + "calculationFormula": "Interest = (ADB × APR / 365) × Days_in_Billing_Cycle; Late fee = $35.00 if payment_received_date > due_date + 2 days (UTC).", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "false", + "mfaRequired": "false", + "rateLimitBucket": "N/A", + "inputFields": "account_id,statement_id,format", + "validationRules": "Owner-only access to statements; format JSON or PDF; nonexistent statement returns 404", + "errorCodesCovered": "NOT_FOUND, FORBIDDEN", + "stateTransitions": "Billing cycle close -> statement generated with computed fields", + "dataMaskingChecks": "No PAN exposure in statement JSON/PDF; masked PAN if present", + "auditTrailChecks": "ASSUMPTION: Statement generation and recalculations logged", + "piiFields": "Account ownership linkage only; no sensitive PII in errors", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, PII minimal", + "assumptions": "ASSUMPTION: Test harness can set payment_received_date and prev_statement_balance_paid_in_full flags; ASSUMPTION: Owner-only enforced consistently; weekends/holidays do not alter late fee logic.", + "testData": "APR 19.99%, sample adb and cycle length (e.g., 30 days), due_date known, nonexistent_statement_id a random UUID", + "cleanupSteps": "Revert any test harness overrides for payments and flags.", + "dependencies": "Statement generation schedule; access to a statements list or portal UI to obtain IDs" + }, + { + "type": "functional", + "title": "Audit Trail for Credit Limit Changes: Immutable Logging, Owner Visibility, Access Control", + "description": "Validate NFR-04 audit trail for credit_limit changes including user_id, session_id, ip_address, timestamp_utc; confirm account summary reflects new limit/available_credit; enforce 403 for non-owner audit access.", + "testId": "TC-AUDIT-LIMIT-015", + "testDescription": "Ensures credit_limit modifications are audited immutably and visible via an admin/audit interface; confirms no user-level endpoint allows limit change and that available_credit aligns with new limit.", + "prerequisites": "Active account with current_balance known; admin test credentials for audit view; authenticated cardholder session for summary checks.", + "stepsToPerform": "1. As the cardholder, GET /v2/accounts/{account_id}/summary; record credit_limit L0, current_balance B0, available_credit A0, account_status Active.\n2. As an admin (ASSUMPTION: admin role), PATCH /v2/admin/accounts/{account_id}/credit-limit with new credit_limit L1 = L0 + 1000.00 and include admin CSRF token (ASSUMPTION); expect 200 and success confirmation.\n3. As the cardholder, GET /v2/accounts/{account_id}/summary again; verify credit_limit equals L1 and available_credit updated approximately A0 + 1000.00 (accounting for current_balance), with no discrepancies.\n4. Trigger a small PURCHASE of $10.00 CAD via POST /v2/accounts/{account_id}/transactions (with X-CSRF-Token) to confirm available_credit decrements from the new limit baseline; expect 200.\n5. As admin, GET /v2/admin/audit?entity=credit_limit&account_id={account_id}; verify an audit record exists for the change showing old_value L0, new_value L1, user_id (admin), session_id, ip_address, timestamp_utc, and immutable hash or sequence ID (ASSUMPTION).\n6. As the cardholder (non-admin), attempt to access the same audit endpoint; expect 403 FORBIDDEN with no data exposure.\n7. As the cardholder, attempt to change credit_limit via a non-existent or blocked user endpoint (e.g., PATCH /v2/accounts/{account_id}/credit-limit); expect 404 NOT_FOUND or 403 FORBIDDEN, confirming no user path exists.\n8. Validate no full PAN appears in audit payloads or summaries; any card references are masked as **** **** **** 1234 (REQ-014).\n9. Repeat admin credit_limit change to L2 = L1 - 500.00 (still >= current_balance) and confirm a second audit entry appended (immutable history), then verify updated limit in summary matches L2.\n10. Confirm timestamps are ISO 8601 UTC in audit entries and that records cannot be altered by a subsequent admin call (attempted update should be rejected by design, ASSUMPTION).", + "expectedResult": "Account summary reflects new credit_limit and available_credit; audit trail captures each change with required metadata and is immutable; non-admin access to audit fails with 403; no user-level endpoint allows limit changes; PAN masking enforced.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/admin/accounts/{account_id}/credit-limit, /v2/admin/audit", + "httpMethod": "GET, POST, PATCH, GET", + "endpointGroup": "Accounts, Transactions, Admin", + "workflow": "Account Dashboard & Card Management", + "businessRuleIds": "NFR-04, REQ-014", + "calculationFormula": "available_credit = credit_limit - current_balance (simplified, excluding pending/holds).", + "rolesCovered": "Cardholder, Admin", + "authRequired": "true", + "csrfRequired": "true for PATCH/POST", + "mfaRequired": "ASSUMPTION: true for admin-sensitive actions", + "rateLimitBucket": "standard", + "inputFields": "credit_limit,new_limit,account_id,admin_csrf", + "validationRules": "Owner-only account summary; admin-only credit_limit change; audit records immutable with user_id,session_id,ip,timestamp", + "errorCodesCovered": "FORBIDDEN, NOT_FOUND", + "stateTransitions": "Credit limit L0->L1->L2; audit log length increments per change", + "dataMaskingChecks": "PAN masked everywhere; no sensitive PII in audit output beyond required metadata", + "auditTrailChecks": "Audit entries contain user_id, session_id, ip_address, timestamp_utc, old/new values, immutable identifier", + "piiFields": "user_id in audit, ip_address (considered sensitive logging metadata)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Audit Trail, Security", + "assumptions": "ASSUMPTION: Admin endpoints exist for credit limit updates and audit retrieval; ASSUMPTION: Audit store is immutable and queryable; ASSUMPTION: Admin actions require CSRF and potentially MFA.", + "testData": "L0 current limit from summary; L1 = L0 + 1000; L2 = L1 - 500; transaction $10.00 CAD", + "cleanupSteps": "Restore original credit_limit L0 via admin endpoint; verify final audit entry documents the restoration.", + "dependencies": "Admin credentials; audit service availability; CSRF issuance for admin" + }, + { + "type": "functional", + "title": "Phone OTP Verification Flow: Request, Verify, Expiry, Resend Throttle, Attempts Remaining", + "description": "Validate phone OTP lifecycle for application phone verification including invalid code, expiry handling, resend rate limiting, attempts_remaining decrement, and successful verification flagging with no PII leakage.", + "testId": "TC-OTP-PHONE-016", + "testDescription": "Ensures OTP endpoints enforce 6-digit format, throttle resends, expire codes, track attempts_remaining, and set phone_verified=true only upon correct OTP while masking PII.", + "prerequisites": "Authenticated user user@example.com with verified email; valid access_token cookie; phone_number +14165550123 set on profile or Step 1; CSRF token available if required by implementation.", + "stepsToPerform": "1. ASSUMPTION: Call POST /v2/auth/otp/request with channel=SMS and purpose=PHONE_VERIFY; expect 200 and delivery metadata; capture request_id for correlation.\n2. Attempt POST /v2/auth/otp/verify with a non-numeric code '12A45B'; expect 400 validation error for format; confirm attempts_remaining unchanged (or decremented only on valid-shaped attempts per policy; document behavior).\n3. Submit POST /v2/auth/otp/verify with wrong 6-digit code '000000'; expect 401 OTP_FAILED and attempts_remaining decremented by 1.\n4. Immediately POST /v2/auth/otp/verify with another wrong code; expect 401 OTP_FAILED; verify attempts_remaining decremented again; ensure no phone_verified flag.\n5. Trigger POST /v2/auth/otp/request twice more within 60 seconds; the second call should return 429 RATE_LIMITED or 400 TOO_MANY_REQUESTS (ASSUMPTION) with retry_after header; ensure no new code issued on throttle.\n6. Wait until throttle window passes; POST /v2/auth/otp/request again; expect 200 and a new OTP generated; verify prior OTP becomes invalid.\n7. Wait until OTP expiry window passes (e.g., 5 minutes; ASSUMPTION); attempt POST /v2/auth/otp/verify with the expired but otherwise correct code; expect 401 OTP_FAILED with reason expired.\n8. Request a fresh OTP via POST /v2/auth/otp/request; expect 200; immediately verify via POST /v2/auth/otp/verify with the correct code; expect 200 phone_verified=true in user/application context.\n9. Validate that logs or response do not expose full phone number beyond masked format (e.g., +1******0123) and no PII like ssn_last4 or tokens are echoed; ensure HttpOnly, Secure cookies remain set; no tokens in localStorage.\n10. Attempt to reuse the same OTP after success; expect 401 OTP_FAILED (one-time use) and no change to verification state.", + "expectedResult": "OTP request and verification enforce 6-digit numeric codes; invalid format rejected; wrong codes decrement attempts_remaining; resend throttled; expired codes fail; correct code marks phone_verified=true; OTP one-time use enforced; no PII leaks; security cookies intact.", + "apiPath": "/v2/auth/otp/request, /v2/auth/otp/verify", + "httpMethod": "POST, POST", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-05 for CSRF if applicable, Security OTP policy", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "ASSUMPTION: false for OTP endpoints, true if policy enforces CSRF on POST", + "mfaRequired": "true for OTP verification step itself", + "rateLimitBucket": "OTP request throttle per phone/device (ASSUMPTION)", + "inputFields": "channel,purpose,code,request_id", + "validationRules": "OTP must be exactly 6 digits; resend throttle window enforced; OTP expiry window enforced; one-time use", + "errorCodesCovered": "OTP_FAILED, RATE_LIMITED or TOO_MANY_REQUESTS, 400 field validation", + "stateTransitions": "phone_verified: false->true upon successful verification; attempts_remaining decremented on failures", + "dataMaskingChecks": "Phone masked in responses/logs; no ssn_last4, tokens, or PAN in responses; HttpOnly, Secure cookies only", + "auditTrailChecks": "ASSUMPTION: OTP requests and verifications logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "phone_number", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: OTP endpoints exist with purpose PHONE_VERIFY and provide attempts_remaining; ASSUMPTION: Expiry and throttle windows are configurable and visible via responses or logs.", + "testData": "phone +14165550123; invalid codes '12A45B' and '000000'; valid OTP retrieved from test harness or SMS stub", + "cleanupSteps": "None; OTPs expire automatically", + "dependencies": "SMS or OTP delivery stub available; server clock reliable" + }, + { + "type": "functional", + "title": "Application Step 1 Validation, Email Match, Address Schema, Autosave Privacy and Cross-User Isolation", + "description": "Verify Step 1 enforces email matching the authenticated user, address schema rules, ID field enumerations and lengths, duplicate application detection, and that autosave stores only non-sensitive fields and is isolated per user.", + "testId": "TC-APP-STEP1-VALID-017", + "testDescription": "Focuses on Step 1 Personal Info validations and the autosave mechanism: privacy, non-sensitive data only, namespace isolation across users, and duplicate application behavior.", + "prerequisites": "Two verified users exist: userA@example.com and userB@example.com; both can log in; browser localStorage accessible; CSRF token available.", + "stepsToPerform": "1. Login as userA@example.com; obtain CSRF token; open application form Step 1.\n2. POST /v2/applications/start with email set to userB@example.com (mismatch), valid phone +14165550111, valid address, id_type PASSPORT, id_number 'X1234567890'; expect 400 with field=email must match authenticated user.\n3. Retry with email userA@example.com but invalid province 'Ontario' instead of 2-char code; expect 400 with field=address.province.\n4. Retry with province 'ON' but invalid postal_code '12345'; expect 400 with field=address.postal_code (must be Canadian A1A 1A1).\n5. Retry with valid address but id_type 'NATIONAL_ID' (not in {PASSPORT, DRIVERS_LICENSE, PR_CARD}); expect 400 invalid enumeration.\n6. Retry with id_type DRIVERS_LICENSE but id_number length 25 chars (over 20); expect 400 field length validation.\n7. Submit a fully valid payload: email userA@example.com, proper address (e.g., A1A 1A1), id_type PASSPORT, id_number 'X1234567'; expect 201 with application_id and session_token.\n8. Wait for autosave interval (60s) to trigger; reload the page; verify draft restored for non-sensitive fields (name, address) and confirm ssn_last4 or any sensitive fields are not stored in localStorage or visible in DOM.\n9. Logout userA; login as userB; navigate to application; assert no draft from userA is visible and that localStorage keys are namespaced or cleared; start Step 1 for userB with valid data and capture application_id_B.\n10. While logged in as userB, attempt to POST /v2/applications/start again with duplicate Step 1 data immediately; expect 409 DUPLICATE_APPLICATION; ensure no second application created.\n11. Switch back to userA and call POST /v2/applications/start again; expect 409 DUPLICATE_APPLICATION for userA's active application; verify no cross-user leakage of application_id values in UI or storage.", + "expectedResult": "Step 1 rejects email mismatch, invalid province/postal code, invalid id_type, overlength id_number; valid submission returns application_id and session_token; autosave persists only non-sensitive fields; drafts are isolated per user; duplicate application attempts return 409 without creating new records.", + "apiPath": "/v2/applications/start", + "httpMethod": "POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002 step order context, NFR-05 CSRF, REQ-014 masking (no sensitive data in UI)", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true if portal policy requires", + "rateLimitBucket": "applications standard", + "inputFields": "full_legal_name,email,phone_number,address.street,address.city,address.province,address.postal_code,id_type,id_number", + "validationRules": "email must equal authenticated user email; province 2-char code; postal_code matches A1A 1A1; id_type in allowed set; id_number <= 20 chars", + "errorCodesCovered": "DUPLICATE_APPLICATION, 400 field validation errors", + "stateTransitions": "Application: none->STEP1 (active draft)", + "dataMaskingChecks": "No ssn_last4 or PAN in DOM/localStorage; masked PAN only if shown anywhere", + "auditTrailChecks": "ASSUMPTION: Application creation logged with user_id, session_id, ip_address", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Autosave uses localStorage and excludes sensitive fields by design; duplicate application detection applies per user.", + "testData": "userA@example.com, userB@example.com; postal_code 'A1A 1A1'; province 'ON'", + "cleanupSteps": "Archive or delete test applications via test harness", + "dependencies": "Products catalog not required for Step 1; Portal autosave enabled" + }, + { + "type": "functional", + "title": "Application Step 2 Soft Credit Pull: sin_consent Enforcement, Conditional Employer, Bureau Failure Retry, Session Token Expiry", + "description": "Validate Step 2 financials require sin_consent=true, enforce employer_name when EMPLOYED, handle bureau 503 retry without duplicating pulls, and reject expired or tampered session_token.", + "testId": "TC-APP-CREDITPULL-018", + "testDescription": "Focuses on Step 2 data and soft credit pull behavior including backend error handling and session_token expiry boundary.", + "prerequisites": "User logged in as user@example.com; Step 1 completed with application_id and session_token; CSRF token available; credit bureau stub controllable to simulate 503 and success.", + "stepsToPerform": "1. POST /v2/applications/{application_id}/financials with X-App-Session set to valid session_token, employment_status EMPLOYED, employer_name omitted, gross_annual_income 60000.00, monthly_rent 1200.00, existing_debt_payments 200.00, sin_consent true; expect 400 field=employer_name required when EMPLOYED.\n2. Resubmit with employer_name 'Aegis Corp' but sin_consent=false; expect 400 error indicating sin_consent must be true.\n3. Resubmit with all required fields valid and sin_consent=true; set bureau stub to return 503 TEMPORARY_FAILURE; expect 200 status=PENDING_REVIEW with fico_pull_id queued but not yet resolved, and perhaps retry_after metadata (ASSUMPTION).\n4. Immediately resubmit identical Step 2 payload; expect idempotent handling (200 with same fico_pull_id) and no duplicate bureau pulls.\n5. After a short delay, set bureau stub to return success FICO=650; trigger backend retry (ASSUMPTION: automatic) or POST a Step 2 confirm call if available (ASSUMPTION); ensure application status updates on next Step 3 to PENDING per thresholds.\n6. Attempt POST /v2/applications/{application_id}/financials with a tampered X-App-Session value; expect 401 SESSION_EXPIRED and no new fico_pull_id.\n7. Advance time 31 minutes to expire the original session_token; resubmit the valid payload with the now-expired token; expect 401 SESSION_EXPIRED.\n8. Start a fresh Step 1 for a new application (or ASSUMPTION: obtain refreshed session_token for the existing application via UI flow) and capture the new session_token; resubmit Step 2 successfully and receive 200 with status=PENDING_REVIEW and fico_pull_id.\n9. Proceed to Step 3 submit with card_product_id and e_signature; with FICO=650 expect decision PENDING; confirm marketing_opt_in default false if omitted and no full PAN returned.", + "expectedResult": "Step 2 enforces sin_consent and employer_name rules; bureau 503 yields queued PENDING_REVIEW without duplication; tampered or expired session_token returns 401; with valid token and FICO=650, Step 3 returns PENDING; no sensitive data exposed.", + "apiPath": "/v2/applications/{application_id}/financials, /v2/applications/{application_id}/submit, /v2/applications/start", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Applications", + "workflow": "Credit Application Web Flow", + "businessRuleIds": "REQ-002, NFR-05 CSRF", + "calculationFormula": "Decision thresholds per SRS: FICO > 680 APPROVED; 600-680 PENDING; <600 DECLINED.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true if portal policy requires", + "rateLimitBucket": "applications standard", + "inputFields": "employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,card_product_id,e_signature,X-App-Session", + "validationRules": "employer_name required when EMPLOYED; sin_consent must be true; session_token must be valid, unexpired, and unmodified", + "errorCodesCovered": "SESSION_EXPIRED, 400 field validation errors", + "stateTransitions": "Application: STEP1->STEP2 PENDING_REVIEW->SUBMITTED->PENDING", + "dataMaskingChecks": "No PAN or bureau raw payload in UI or responses; masked PAN only when applicable", + "auditTrailChecks": "ASSUMPTION: Credit pull initiation and decisions logged with user_id, session_id, ip_address", + "piiFields": "employment data, income amounts", + "riskLevel": "High", + "regulatoryTags": "PII, Security", + "assumptions": "ASSUMPTION: Bureau stub can be configured; ASSUMPTION: Retrying behavior is automatic or via a documented mechanism; ASSUMPTION: New session_token can be obtained if prior expired.", + "testData": "employment_status EMPLOYED; income 60000.00; rents/debts per steps; FICO=650", + "cleanupSteps": "Archive applications created during test", + "dependencies": "Credit bureau stub; time travel or wait controls; CSRF issuance" + }, + { + "type": "functional", + "title": "Notifications Webhook Validation: Invalid alert_type/channel, PII Masking in message_body, Severity Rendering", + "description": "Validate the notifications webhook rejects unknown alert_type/channel, masks PII in message_body, delivers alerts across channels, and renders proper severity in portal UI.", + "testId": "TC-NOTIFY-VALID-019", + "testDescription": "Ensures webhook input validation and PII masking, channel handling, and UI severity badge mapping operate correctly without idempotency conflicts.", + "prerequisites": "Active account_id for user@example.com; authenticated portal session open to observe in-app notifications; webhook endpoint reachable.", + "stepsToPerform": "1. POST /v2/notifications/webhook with account_id, alert_type 'UNKNOWN_EVENT', channel IN_APP, message_body 'Test', severity INFO, idempotency_key a valid UUID; expect 400 INVALID_ALERT_TYPE.\n2. POST /v2/notifications/webhook with valid alert_type LATE_PAYMENT but channel 'PAGER' (unsupported); expect 400 INVALID_ALERT_TYPE or invalid channel error per spec.\n3. POST /v2/notifications/webhook with alert_type STATEMENT_READY, channel IN_APP, severity INFO, message_body 'Your statement is ready', idempotency_key UUID1; expect 200 notification_id and delivered_at; verify portal shows INFO badge.\n4. POST /v2/notifications/webhook with alert_type OVER_LIMIT, channel EMAIL, severity WARNING, message_body 'Over-limit used on card **** **** **** 1234', idempotency_key UUID2; expect 200 queued or delivered; confirm no full PAN appears in any payload or UI.\n5. POST /v2/notifications/webhook with alert_type FRAUD_FLAG, channel IN_APP, severity CRITICAL, message_body 'Suspicious charge on card 4111 1111 1111 1111 at Merchant X', idempotency_key UUID3; expect 200; verify backend masks PAN in stored/displayed message to **** **** **** 1111 (REQ-014) and portal shows CRITICAL badge.\n6. In portal, refresh the notifications UI and verify exactly three alerts visible with correct types and severities INFO, WARNING, CRITICAL; ensure timestamps are ISO 8601 UTC and sorted by delivered_at.\n7. Attempt to send a very long message_body >500 chars; expect 400 validation error and no alert created.\n8. Confirm that in-app alert payloads contain no PII beyond masked PAN and merchant descriptors; no ssn_last4, address, or tokens present.\n9. Resend the STATEMENT_READY with a different idempotency_key UUID4 but same content; expect 200 and a second distinct alert, proving idempotency depends on key not content.", + "expectedResult": "Webhook rejects unknown alert_type and channels; enforces message_body length; masks PAN found in message_body; delivers alerts per channel; UI displays correct severities; idempotency keyed by idempotency_key; no PII leakage.", + "apiPath": "/v2/notifications/webhook", + "httpMethod": "POST", + "endpointGroup": "Notifications", + "workflow": "Notifications & Alerts", + "businessRuleIds": "REQ-014 masking, NFR-05 CSRF not required for internal webhook", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder (UI consumer), Notification Engine (caller)", + "authRequired": "false for webhook caller (internal trust), true for portal viewing", + "csrfRequired": "false for webhook (internal), true for portal state changes", + "mfaRequired": "false", + "rateLimitBucket": "webhook internal standard", + "inputFields": "account_id,alert_type,channel,severity,message_body,idempotency_key", + "validationRules": "alert_type and channel must be from enumerations; message_body <= 500 chars; idempotency_key UUID v4", + "errorCodesCovered": "INVALID_ALERT_TYPE, 400 validation error", + "stateTransitions": "Notification: none->queued/delivered; UI: unread->read (not exercised)", + "dataMaskingChecks": "PAN masking enforced in messages; no ssn_last4 or sensitive PII shown", + "auditTrailChecks": "ASSUMPTION: Notification deliveries logged with account_id, idempotency_key, timestamp_utc", + "piiFields": "account_id linking only; message must not contain unmasked PAN", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, PII, Security", + "assumptions": "ASSUMPTION: Webhook is authenticated internally (mutual TLS or allowlist) and not CSRF-scoped; UI polls or receives push to render alerts.", + "testData": "UUID1, UUID2, UUID3, UUID4 as unique idempotency keys; sample messages containing PAN-like strings", + "cleanupSteps": "Clear test notifications from non-prod environment if needed", + "dependencies": "Portal notifications UI; backend masking filter enabled" + }, + { + "type": "functional", + "title": "Transactions Listing Date/Time Boundaries, Same-day Range, DST/UTC Handling, per_page Max Validation", + "description": "Validate transaction list filtering at time boundaries including same-day ranges, UTC handling around DST changes, stable pagination, and per_page max >100 rejection.", + "testId": "TC-TRX-LIST-DATERANGE-020", + "testDescription": "Exercises GET transactions parameters for date ranges and pagination limits without overlapping prior invalid-range tests; ensures accurate inclusion on boundary timestamps and rejection of per_page=101.", + "prerequisites": "Active account_id with at least three known transactions at controlled timestamps; authenticated session; CSRF token for creating new transactions during setup.", + "stepsToPerform": "1. Create three PURCHASE transactions via POST /v2/accounts/{account_id}/transactions with timestamps set by test harness (ASSUMPTION): T1=2026-03-14T00:00:00Z, T2=2026-03-14T23:59:59Z, T3=2026-03-15T12:00:00Z; ensure 200 approvals.\n2. GET /v2/accounts/{account_id}/transactions with from_date=2026-03-14, to_date=2026-03-14, page=1, per_page=25; expect results include exactly T1 and T2 but not T3 (same-day boundary inclusion for start and end).\n3. GET /v2/accounts/{account_id}/transactions with from_date=2026-03-15, to_date=2026-03-15, page=1, per_page=25; expect T3 present only.\n4. Around DST change window (ASSUMPTION: 2026-03-08 for North America), create two additional PURCHASE transactions at 2026-03-08T01:59:59Z and 2026-03-08T03:00:01Z; GET with from_date=2026-03-08, to_date=2026-03-08; verify both included, confirming UTC-based filtering is consistent and unaffected by DST.\n5. GET /v2/accounts/{account_id}/transactions with from_date unspecified and to_date=2026-03-14; expect default from_date billing cycle start and inclusion up to 2026-03-14 end of day.\n6. GET /v2/accounts/{account_id}/transactions with page=1, per_page=101; expect 400 validation error for per_page max 100.\n7. Populate >25 transactions (ASSUMPTION via looped POST) and GET page=1 per_page=25 then page=2 per_page=25; verify no duplicate items across pages and stable ordering (document default sort order, typically most recent first).\n8. GET /v2/accounts/{account_id}/transactions with category REFUND; if none exist, expect 200 with empty transactions[]; confirm API handles empty result gracefully.\n9. Confirm responses include total_count, page, total_pages fields; validate that sum of items across pages equals total_count for the given filter.\n10. Ensure responses contain masked PAN only where any card reference is shown and that no PII like ssn_last4 appears.", + "expectedResult": "Same-day ranges include both start and end boundary timestamps; UTC/DST boundaries do not exclude valid transactions; per_page=101 rejected; pagination stable without duplicates; empty category results return 200 with empty list; outputs include pagination metadata and mask PAN.", + "apiPath": "/v2/accounts/{account_id}/transactions", + "httpMethod": "POST, GET", + "endpointGroup": "Transactions", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-014 masking", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST, false for GET", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "from_date,to_date,page,per_page,category,transaction_amount,merchant_name,merchant_id,mcc_code", + "validationRules": "per_page max 100; page min 1; date filters inclusive for same day; UTC timestamps used", + "errorCodesCovered": "400 validation error for per_page", + "stateTransitions": "None (read-only for GET); transactions created for setup reduce available_credit accordingly", + "dataMaskingChecks": "Masked PAN in any transaction representations; no sensitive PII in responses", + "auditTrailChecks": "ASSUMPTION: Transaction creations logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "None beyond account ownership linkage", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Test harness can set transaction timestamps for deterministic boundary testing; default list sort is consistent.", + "testData": "Boundary timestamps 2026-03-14T00:00:00Z and 23:59:59Z; DST window events on 2026-03-08", + "cleanupSteps": "Leave transactions for later statement validation or purge via test harness if needed", + "dependencies": "Time control or backdated posting supported in non-prod; sufficient available_credit" + }, + { + "type": "functional", + "title": "OAuth2 Authorization Code with PKCE: State Integrity, Code Verifier, Cookie Storage, Replay Defense", + "description": "Validate full OAuth2 Authorization Code with PKCE browser flow: proper state parameter handling, code_verifier -> code_challenge(S256) validation, redirect_uri checks, single-use auth code, tokens only in HttpOnly Secure cookies, and logout CSRF.", + "testId": "TC-AUTH-PKCE-021", + "testDescription": "Ensures compliance with OAuth2 + PKCE security: state matches, invalid code_verifier rejected, authorization code replay blocked, cookies have HttpOnly/Secure/SameSite=Strict; no tokens in localStorage; logout uses CSRF and invalidates refresh.", + "prerequisites": "Browser on TLS 1.3; registered and email-verified user user@example.com with MFA enabled; device_id available; portal configured for OAuth2 Authorization Code with PKCE; CSRF issuance endpoint available.", + "stepsToPerform": "1. Generate code_verifier (high-entropy 43-128 chars) and compute code_challenge=BASE64URL(SHA256(code_verifier)).\n2. Navigate to GET /v2/oauth/authorize?response_type=code&client_id=portal&redirect_uri=https://portal.aegiscard.com/callback&scope=openid%20profile&state=xyz123&code_challenge_method=S256&code_challenge= over TLS 1.3; verify HSTS and no mixed content (ASSUMPTION endpoints).\n3. On login form, enter user@example.com and correct password; complete TOTP 6-digit challenge; submit; expect redirect back with code and state=xyz123 intact.\n4. Validate state integrity: ensure returned state exactly matches xyz123; if tampered state provided in step 2 (negative retry), expect 400 INVALID_STATE and no code issued.\n5. Exchange code via POST /v2/oauth/token with grant_type=authorization_code, code=, redirect_uri matching original, client_id portal, code_verifier=; expect 200 and access/refresh tokens set as HttpOnly, Secure, SameSite=Strict cookies; no token body returned to JS (ASSUMPTION cookie delivery).\n6. Attempt token exchange again reusing the same code (replay); expect 400 or 401 AUTH_CODE_REDEEMED and no cookies changed.\n7. Attempt token exchange using wrong redirect_uri; expect 400 INVALID_REDIRECT_URI and no cookies set.\n8. Attempt token exchange using an invalid code_verifier that does not match code_challenge; expect 400 INVALID_CODE_VERIFIER and no cookies.\n9. Verify in browser devtools: cookies have HttpOnly, Secure, SameSite=Strict; window.localStorage and sessionStorage contain no access/refresh tokens; Authorization header is not persisted in JS scope.\n10. Access a protected API GET /v2/accounts/{account_id}/summary; expect 200; then POST /v2/auth/logout with X-CSRF-Token; expect 200 and cookies expired.\n11. After logout, attempt POST /v2/auth/token/refresh; expect 401 TOKEN_INVALID; accessing protected summary again returns 401 UNAUTHORIZED.", + "expectedResult": "PKCE flow succeeds only with valid state and code_verifier; authorization code is single-use; tokens are delivered via Secure, HttpOnly, SameSite=Strict cookies; no tokens in web storage; logout with CSRF clears cookies and refresh reuse is rejected.", + "apiPath": "/v2/oauth/authorize, /v2/oauth/token, /v2/auth/logout, /v2/accounts/{account_id}/summary", + "httpMethod": "GET, POST, POST, GET", + "endpointGroup": "Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-01, NFR-05, NFR-06", + "calculationFormula": "code_challenge = BASE64URL(SHA256(code_verifier))", + "rolesCovered": "Cardholder", + "authRequired": "false for authorize/token, true for summary", + "csrfRequired": "true for logout", + "mfaRequired": "true at login prompt", + "rateLimitBucket": "login 10 req/min per IP", + "inputFields": "client_id,redirect_uri,scope,state,code_verifier,code_challenge_method,code_challenge,code", + "validationRules": "state must echo back unchanged; redirect_uri must match exactly; code single-use; code_verifier must match code_challenge S256; cookies must be Secure, HttpOnly, SameSite=Strict", + "errorCodesCovered": "INVALID_STATE, INVALID_REDIRECT_URI, INVALID_CODE_VERIFIER, TOKEN_INVALID, UNAUTHORIZED", + "stateTransitions": "Auth: unauthenticated->authorized->logged_out", + "dataMaskingChecks": "No tokens visible to JS; no PII in OAuth errors", + "auditTrailChecks": "ASSUMPTION: Authorization and token issuance logged with user_id, session_id, ip_address, timestamp_utc", + "piiFields": "email", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: /v2/oauth/authorize and /v2/oauth/token exist; tokens are cookie-delivered; portal uses PKCE S256.", + "testData": "email user@example.com, device_id 550e8400-e29b-41d4-a716-446655440010, state xyz123", + "cleanupSteps": "Logout; clear cookies; reset any lockout counters if tripped", + "dependencies": "OAuth server configured for PKCE; CSRF token endpoint available" + }, + { + "type": "functional", + "title": "Transaction Rate-Limit Recovery via MFA: 429 FREQ_EXCEEDED -> MFA Verify -> Counter Reset", + "description": "When >10 transactions in 60 minutes, API returns 429 with mfa_required=true; validate MFA challenge flow to restore ability to transact and confirm headers and counters reset.", + "testId": "TC-TXN-MFA-022", + "testDescription": "Executes rapid purchases to trigger transaction rate limit, performs MFA verification to lift restriction, and confirms subsequent transactions succeed with correct available_credit updates and proper Retry-After handling.", + "prerequisites": "Active account with sufficient available_credit; authenticated session with CSRF; OTP/TOTP delivery working; WebSocket optional.", + "stepsToPerform": "1. Confirm baseline via GET /v2/accounts/{account_id}/summary; capture available_credit and account_status Active.\n2. Perform 10 quick POST /v2/accounts/{account_id}/transactions PURCHASEs (CAD) within 60 minutes with X-CSRF-Token; expect 200 approvals and available_credit decrements.\n3. Attempt the 11th POST transaction within the same 60-minute window; expect 429 FREQ_EXCEEDED with mfa_required=true and Retry-After header present (seconds).\n4. Immediately retry the 11th transaction without completing MFA; expect 429 again until limit window or MFA passed; verify no duplicate postings created.\n5. Initiate MFA verification flow: POST /v2/auth/otp/request with purpose=TRANSACTION_RATE_LIMIT (ASSUMPTION) and channel=SMS; expect 200 with delivery metadata.\n6. Submit POST /v2/auth/otp/verify with correct 6-digit code; expect 200 and a response or flag indicating rate-limit lift (ASSUMPTION: transactions_mfa_cleared=true) and attempts_remaining unaffected on success.\n7. Retry the previously blocked transaction POST; expect 200 approval with transaction_id, auth_code, available_credit updated.\n8. Trigger one more purchase to confirm the counter effectively reset by MFA and no additional 429 occurs; verify header X-RateLimit-Remaining or equivalent reflects reset (ASSUMPTION).\n9. Negative: submit an invalid OTP before success; expect 401 OTP_FAILED and rate-limit remains; validate attempts_remaining decrements and subsequent valid OTP still works.\n10. Confirm audit/log entries exist for limit trigger and MFA clearance (ASSUMPTION); verify no PII leaks in error payloads.", + "expectedResult": "429 FREQ_EXCEEDED is returned on the 11th transaction with mfa_required=true; after successful OTP verification, subsequent transactions are permitted immediately and counters reset; invalid OTP does not lift the limit; Retry-After is provided on 429 responses.", + "apiPath": "/v2/accounts/{account_id}/transactions, /v2/auth/otp/request, /v2/auth/otp/verify", + "httpMethod": "POST, POST, POST", + "endpointGroup": "Transactions, Auth", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006 foreign fee not exercised here, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST transactions", + "mfaRequired": "true to clear rate limit", + "rateLimitBucket": "transactions >10 in 60 min -> 429 FREQ_EXCEEDED, mfa_required true", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,transaction_type,channel,purpose,code", + "validationRules": "transaction_amount > 0; CSRF required; OTP must be 6 digits; MFA verification unlocks rate-limit", + "errorCodesCovered": "FREQ_EXCEEDED, OTP_FAILED", + "stateTransitions": "Rate-limit: normal->limited->cleared after MFA", + "dataMaskingChecks": "No PAN in error messages; masked PAN if present in any transaction payloads", + "auditTrailChecks": "ASSUMPTION: Limit trigger and MFA clearance logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "phone_number (masked in logs), account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: OTP purpose TRANSACTION_RATE_LIMIT supported; Retry-After header present; rate-limit can be cleared by MFA.", + "testData": "Amounts CAD $1.00 each, mcc_code 5999, merchant_id INCR001..INCR011", + "cleanupSteps": "None; keep transactions for later statement validation", + "dependencies": "OTP delivery channel; CSRF token available" + }, + { + "type": "functional", + "title": "PAN Masking and Iframe Tokenization Compliance Sweep Across Portal Surfaces", + "description": "End-to-end verification that full PAN never appears in frontend DOM, network logs, or storage; all displays show **** **** **** 1234; card fields use iframe tokenization; masking holds across API responses, WebSocket events, statements, notifications.", + "testId": "TC-PCI-MASK-023", + "testDescription": "Per REQ-014 and NFR-01, validate PAN masking everywhere and iframe tokenization integration; inspect DOM, network requests, WebSocket frames, statements JSON/PDF, notifications content, and storage for leaks.", + "prerequisites": "Active account with transactions; authenticated portal session; WebSocket subscription available; statements exist; notifications present that mention card references.", + "stepsToPerform": "1. Navigate to account dashboard; visually confirm card display shows masked PAN like **** **** **** 1234; inspect DOM elements and ensure no hidden full PAN or data attributes contain PAN.\n2. Open browser devtools Network tab and reload dashboard; filter for API calls; verify no response payload contains full PAN; any card references are masked; response caching does not store PAN.\n3. Inspect window.localStorage and sessionStorage; ensure no PAN, ssn_last4, or JWT tokens are stored; only non-sensitive UI preferences allowed.\n4. Navigate to card management UI; verify card number field (if present) is rendered via third-party iframe tokenization component (e.g., Stripe Elements) loaded over TLS 1.3; confirm iframe origin allowlisted by CSP and that host page never receives raw PAN (ASSUMPTION tokenization present).\n5. Create a CAD transaction via POST /v2/accounts/{account_id}/transactions to generate a WebSocket event; verify the event payload in the WebSocket frames contains only masked PAN and necessary merchant/amount fields; no PII or tokens included.\n6. Open latest statement JSON via GET /v2/accounts/{account_id}/statements/{statement_id}; confirm any card references are masked; download PDF format and scan text for patterns matching 16-digit PAN; expect only masked values.\n7. Trigger a notification via POST /v2/notifications/webhook with message_body containing a PAN-like string '4111 1111 1111 1111'; verify stored and rendered message masks to **** **** **** 1111 and no full PAN visible.\n8. In the application flow pages, inspect autosave keys in localStorage; confirm no ssn_last4 or card data stored; only non-sensitive application draft fields are present.\n9. Verify CSP headers in responses disallow inline scripts and restrict iframe sources; ensure all tokenization and API endpoints load over HTTPS TLS 1.3; no mixed content.\n10. Perform DOM and network search for regex patterns resembling PAN (\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b); ensure zero matches across visible content and payload bodies except masked format.", + "expectedResult": "Masked PAN is consistently used across UI, APIs, WebSocket, statements, and notifications; no full PAN appears in DOM, network, or storage; iframe tokenization is present and secure; CSP and TLS policies enforced; autosave contains no sensitive data.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/statements/{statement_id}, /v2/notifications/webhook, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "GET, POST, GET, POST, WebSocket", + "endpointGroup": "Accounts, Transactions, Billing, Notifications, Realtime", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "REQ-014, NFR-01, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true for portal and APIs; webhook internal", + "csrfRequired": "true for POST transaction; false for internal webhook", + "mfaRequired": "false", + "rateLimitBucket": "standard", + "inputFields": "message_body with PAN-like string, transaction_amount, merchant_name, mcc_code", + "validationRules": "PAN must never be transmitted to frontend; all displays are masked; tokenization via iframe required for any card inputs", + "errorCodesCovered": "N/A", + "stateTransitions": "None; read/observe-only aside from one transaction and webhook for validation", + "dataMaskingChecks": "Regex searches yield only masked PAN **** **** **** 1234; no ssn_last4 anywhere", + "auditTrailChecks": "ASSUMPTION: Security scans and masking filters logged", + "piiFields": "None beyond masked card reference and account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Iframe tokenization is used for any card entry UI; CSP headers restrict iframe origins; masking filter applies to notifications.", + "testData": "Webhook message containing 4111 1111 1111 1111; transaction CAD $5.00 at MCC 5999", + "cleanupSteps": "Delete test notification from non-prod if needed", + "dependencies": "Realtime service available; statements accessible; CSP configured" + }, + { + "type": "functional", + "title": "Web-Based PIN Set Edge Cases: Numeric Format, Leading Zeros, OTP Expiry, Attempts Throttle, CSRF", + "description": "Validate PIN endpoint enforces exactly 4 numeric digits including leading zeros, rejects non-numeric and whitespace, handles OTP expiry and attempts_remaining, rate-limits excessive attempts, and requires CSRF.", + "testId": "TC-PIN-EDGE-024", + "testDescription": "Focuses on REQ-008 PIN rules beyond mismatch: 0000 allowed, non-numeric rejected, space-trim not allowed, OTP expiry handling, attempts throttle, CSRF requirement, and successful set when all conditions met.", + "prerequisites": "Active card_id not Blocked; authenticated session; CSRF token; OTP delivery available.", + "stepsToPerform": "1. Attempt PUT /v2/cards/{card_id}/pin without X-CSRF-Token using new_pin 1234 and confirm_pin 1234 with valid session_otp; expect 403 CSRF_MISSING and no PIN change.\n2. Retry with X-CSRF-Token but provide new_pin '12 4' (contains whitespace) and confirm_pin '12 4'; expect 400 PIN_FORMAT and error message indicating exactly 4 digits required.\n3. Retry with new_pin '12A4' (non-numeric) and confirm_pin '12A4'; expect 400 PIN_FORMAT; confirm no PIN set.\n4. Retry with new_pin '123' (3 digits) and confirm_pin '123'; expect 400 PIN_FORMAT; then with new_pin '12345' and confirm_pin '12345'; expect 400 PIN_FORMAT.\n5. Attempt with leading zeros: new_pin '0000' and confirm_pin '0000' but use an expired session_otp (wait beyond expiry or use stale code); expect 401 OTP_FAILED with reason expired and attempts_remaining decremented.\n6. Request a fresh OTP via POST /v2/auth/otp/request purpose=PIN_SET (ASSUMPTION); immediately submit PUT with mismatched confirm_pin (0000 vs 0001); expect 400 PIN_MISMATCH and attempts_remaining unchanged (PIN validation precedes OTP consumption or document behavior observed).\n7. Submit two more attempts with wrong OTP codes to simulate attempts throttle; expect 401 OTP_FAILED with attempts_remaining decreasing each time; when attempts_remaining reaches 0, further attempts return 401 OTP_LOCKED or similar (ASSUMPTION) until reset window.\n8. After cooldown or with a new OTP, submit PUT with new_pin '0000' and confirm_pin '0000' and valid session_otp; expect 200 success true with updated_at timestamp.\n9. Verify audit of PIN set (ASSUMPTION) and ensure GET /v2/accounts/{account_id}/summary and UI show no PIN values anywhere; check DOM/storage for absence of PIN or OTP artifacts.\n10. Negative follow-up: attempt to reuse the same (already-used) session_otp; expect 401 OTP_FAILED (one-time use) and no change.", + "expectedResult": "PIN must be exactly 4 numeric digits; leading zeros allowed; non-numeric and whitespace rejected; CSRF required; expired or invalid OTP fails with attempts_remaining decrement and throttle; valid OTP sets PIN successfully; no PIN/OTP leakage in UI or storage.", + "apiPath": "/v2/cards/{card_id}/pin, /v2/auth/otp/request", + "httpMethod": "PUT, POST", + "endpointGroup": "Card Management, Auth", + "workflow": "Card Management", + "businessRuleIds": "REQ-008, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for PUT", + "mfaRequired": "true via session_otp", + "rateLimitBucket": "standard; OTP attempts throttle applies", + "inputFields": "new_pin,confirm_pin,session_otp,channel,purpose", + "validationRules": "PIN exactly 4 digits numeric; OTP 6 digits valid and unexpired; CSRF required; attempts throttle enforced", + "errorCodesCovered": "CSRF_MISSING, PIN_FORMAT, PIN_MISMATCH, OTP_FAILED", + "stateTransitions": "PIN: unset->set", + "dataMaskingChecks": "No PIN or OTP in responses beyond error metadata; no sensitive data in DOM/storage", + "auditTrailChecks": "ASSUMPTION: PIN set event logged with user_id/session_id/ip_address/timestamp_utc", + "piiFields": "None", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: OTP purpose PIN_SET available; attempts_remaining and lockout semantics exposed; CSRF error returns 403.", + "testData": "Valid OTP from test harness; invalid/stale OTP for expiry; CSRF token current", + "cleanupSteps": "None; PIN remains set", + "dependencies": "OTP delivery; CSRF issuance; card not in Blocked state" + }, + { + "type": "functional", + "title": "Report Card STOLEN with Delivery Address Override: OTP Gating, Address Validation, Audit, Irreversibility", + "description": "Validate high-risk Report STOLEN flow requires OTP, supports delivery_address override validation, blocks card irreversibly, prevents further status changes, and schedules replacement with masked details.", + "testId": "TC-REPORT-STOLEN-025", + "testDescription": "Focuses on nuanced lost/stolen handling beyond basic block: OTP requirement, CSRF, address schema for delivery override, audit contents, prevention of unfreeze/activate post-block, and ensuring no PIN operations permitted until replacement.", + "prerequisites": "Active card_id owned by user@example.com; authenticated session; valid CSRF token; OTP delivery available; knowledge of valid Canadian address schema.", + "stepsToPerform": "1. Confirm via GET /v2/accounts/{account_id}/summary that account_status Active and card is Active; capture masked PAN for verification later.\n2. Attempt POST /v2/cards/{card_id}/report-lost with loss_type STOLEN and a delivery_address override but omit X-CSRF-Token; expect 403 CSRF_MISSING and no state change.\n3. Retry with X-CSRF-Token but omit confirm_otp (ASSUMPTION OTP required for high-risk); if endpoint requires OTP in header or body, expect 401 OTP_FAILED or 400 missing OTP; no state change.\n4. Request an OTP via POST /v2/auth/otp/request purpose=CARD_REPORT_STOLEN; expect 200 with delivery metadata; submit POST /v2/cards/{card_id}/report-lost with loss_type STOLEN, valid confirm_otp, and invalid delivery_address fields (province 'Ontario' not 2-char, postal_code '12345'); expect 400 validation errors for address.\n5. Resubmit with valid delivery_address (street/city valid, province 'ON', postal_code 'A1A 1A1'); include last_known_use timestamp in ISO 8601 UTC; expect 200 with blocked_card_id, new_card_eta, case_number; card transitions to Blocked.\n6. Verify audit trail (ASSUMPTION admin/audit) includes user_id, session_id, ip_address, timestamp_utc, loss_type STOLEN, delivery_address override fields masked where applicable (no full PAN), and case_number.\n7. Attempt PATCH /v2/cards/{card_id}/status to Active or Frozen; expect 400 INVALID_TRANSITION; ensure status remains Blocked.\n8. Attempt PUT /v2/cards/{card_id}/pin with valid session_otp; expect 403 CARD_BLOCKED and no change; confirm UI reflects Blocked and PIN actions disabled.\n9. Attempt to report lost/stolen again for the same card; expect 409 ALREADY_BLOCKED; verify idempotent no duplicate cases created.\n10. Confirm in all responses and UI masked PAN only; scan DOM and network logs for absence of full PAN; ensure no PII leakage in errors.\n11. Optional: Verify replacement card shipment details are limited/masked and no ability to transact with the blocked card via POST /v2/accounts/{account_id}/transactions (expect 403 CARD_INACTIVE).", + "expectedResult": "Report STOLEN requires CSRF and OTP; invalid address rejected; valid request blocks card irreversibly and schedules replacement; subsequent status changes and PIN operations are disallowed; duplicate reports return 409; all card references are masked and audits recorded.", + "apiPath": "/v2/cards/{card_id}/report-lost, /v2/cards/{card_id}/status, /v2/cards/{card_id}/pin, /v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/auth/otp/request, /v2/admin/audit", + "httpMethod": "POST, PATCH, PUT, GET, POST, POST, GET", + "endpointGroup": "Card Management, Accounts, Auth, Admin", + "workflow": "Card Status Control", + "businessRuleIds": "REQ-007, REQ-014, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder, Admin (audit view)", + "authRequired": "true", + "csrfRequired": "true for POST/PATCH/PUT", + "mfaRequired": "true via OTP for high-risk action", + "rateLimitBucket": "standard", + "inputFields": "loss_type,confirm_otp,delivery_address.street,delivery_address.city,delivery_address.province,delivery_address.postal_code,last_known_use", + "validationRules": "delivery_address must meet address schema; OTP 6 digits valid; CSRF required; Blocked state irreversible", + "errorCodesCovered": "CSRF_MISSING, OTP_FAILED, INVALID_TRANSITION, ALREADY_BLOCKED, CARD_INACTIVE", + "stateTransitions": "Active->Blocked (irreversible)", + "dataMaskingChecks": "Masked PAN **** **** **** 1234 in all responses/UI; no full PAN in DOM/network", + "auditTrailChecks": "Audit record with user_id, session_id, ip_address, timestamp_utc, loss_type, override address", + "piiFields": "delivery_address (validated, not overexposed), phone masked in OTP context", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF, Audit Trail", + "assumptions": "ASSUMPTION: OTP required for report STOLEN; admin/audit endpoint accessible in non-prod; shipping ETA returned.", + "testData": "Valid Canadian address: 123 King St, Toronto, ON, A1A 1A1; loss_type STOLEN; valid OTP from test harness", + "cleanupSteps": "None; replacement issuance handled by operations in non-prod", + "dependencies": "OTP delivery; audit service available; CSRF issuance" + }, + { + "type": "functional", + "title": "CSRF Regeneration After Refresh Rotation: Old Token Invalid, New Token Required", + "description": "Validate that CSRF tokens are session-bound and become invalid after refresh token rotation; state-changing requests must use a newly issued CSRF token post-rotation.", + "testId": "TC-CSRF-ROTATE-026", + "testDescription": "Ensures POST/PUT/PATCH/DELETE endpoints reject an old X-CSRF-Token after /v2/auth/token/refresh rotates tokens; verifies new CSRF works, cookies flags remain secure, and no data is mutated by the rejected request.", + "prerequisites": "Verified user user@example.com with MFA enabled; active account_id and card_id owned by the user; browser over TLS 1.3; initial authenticated session established with valid access/refresh cookies; a valid CSRF token obtained.", + "stepsToPerform": "1. Confirm initial login over TLS 1.3 and obtain a fresh CSRF via GET /v2/auth/csrf; store token CSRF-0.\n2. Perform a valid state-changing call using CSRF-0, e.g., POST /v2/accounts/{account_id}/transactions with small CAD PURCHASE; expect 200 and transaction_id.\n3. Call POST /v2/auth/token/refresh to rotate tokens; verify 200 and Set-Cookie for new access_token and refresh_token (HttpOnly, Secure, SameSite=Strict); do NOT fetch a new CSRF yet.\n4. Attempt another state-changing call using the stale CSRF-0: PUT /v2/cards/{card_id}/pin with dummy payload (new_pin 1234/confirm_pin 1234 and a valid session_otp); expect 403 CSRF_INVALID or CSRF_MISSING; confirm no PIN change (no success).\n5. Obtain a new CSRF token via GET /v2/auth/csrf; store as CSRF-1; confirm CSRF-1 differs from CSRF-0.\n6. Retry PUT /v2/cards/{card_id}/pin with X-CSRF-Token: CSRF-1 and valid session_otp but with intentionally mismatched pins 1234 vs 4321 to avoid real PIN set; expect 400 PIN_MISMATCH and confirm CSRF accepted (authorization path reached).\n7. Perform a valid state change using CSRF-1, e.g., PATCH /v2/cards/{card_id}/status to Frozen with confirm_otp valid; expect 200 new_status Frozen.\n8. Inspect browser storage to verify no tokens are in localStorage/sessionStorage; check cookies retain HttpOnly, Secure, SameSite=Strict.\n9. Negative: Attempt POST /v2/auth/token/refresh again using the previously used refresh token; expect 401 TOKEN_INVALID; confirm CSRF-1 still valid for current session.\n10. Clean up by unfreezing card via PATCH /v2/cards/{card_id}/status to Active using CSRF-1; expect 200 new_status Active.", + "expectedResult": "Old CSRF token is rejected after refresh rotation; a new CSRF must be fetched and used. No unintended state changes occur with stale CSRF. Cookies remain Secure, HttpOnly, SameSite=Strict. Refresh token reuse is rejected.", + "apiPath": "/v2/auth/csrf, /v2/accounts/{account_id}/transactions, /v2/auth/token/refresh, /v2/cards/{card_id}/pin, /v2/cards/{card_id}/status", + "httpMethod": "GET, POST, POST, PUT, PATCH", + "endpointGroup": "Auth, Transactions, Card Management", + "workflow": "Security & Compliance Controls", + "businessRuleIds": "NFR-05, NFR-06", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "true for OTP-gated endpoints", + "rateLimitBucket": "standard", + "inputFields": "X-CSRF-Token,new_pin,confirm_pin,session_otp,confirm_otp,transaction_amount,merchant_name,merchant_id,mcc_code", + "validationRules": "CSRF tokens are session-bound and must be refreshed after token rotation; PIN must be exactly 4 digits; OTP must be 6 digits.", + "errorCodesCovered": "CSRF_INVALID, CSRF_MISSING, TOKEN_INVALID, PIN_MISMATCH", + "stateTransitions": "Session: pre-refresh->post-refresh; Card: Active->Frozen->Active", + "dataMaskingChecks": "No tokens in localStorage/sessionStorage; masked PAN only in any responses", + "auditTrailChecks": "ASSUMPTION: Status and PIN attempts logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "email (login context only)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: /v2/auth/csrf issues a new CSRF tied to current tokens; ASSUMPTION: CSRF_INVALID 403 is returned for token from previous session.", + "testData": "Small CAD transaction amount $1.00 at MCC 5999; valid OTPs available", + "cleanupSteps": "Return card to Active; clear cookies; revoke tokens", + "dependencies": "CSRF issuance endpoint available; OTP delivery available" + }, + { + "type": "functional", + "title": "Session Timeout Warning: Stay Signed In Flow, Timer Reset, Draft Preservation", + "description": "Validate 13-minute warning modal and user action 'Stay Signed In' prevents auto-logout, refreshes tokens, preserves CSRF and application draft, and keeps session active.", + "testId": "TC-SESSION-STAY-027", + "testDescription": "Ensures that interacting with the warning modal extends session without data loss; verifies cookies flags, CSRF continuity, and that inactivity beyond 15 min triggers auto-logout when the user ignores the warning.", + "prerequisites": "Authenticated portal session with active application draft in Step 1; autosave enabled; valid CSRF token; TLS 1.3; device_id set.", + "stepsToPerform": "1. Login with MFA; confirm Secure, HttpOnly, SameSite=Strict cookies set; fetch CSRF token CSRF-A.\n2. Start application Step 1 POST /v2/applications/start with valid payload; receive application_id and session_token; ensure autosave writes non-sensitive draft in localStorage.\n3. Idle user interactions (no API calls) for 12 minutes; verify session still active by doing a lightweight GET (e.g., a non-mutating endpoint) is not called automatically by UI.\n4. Continue idling until 13 minutes; verify warning modal appears indicating impending logout in 2 minutes.\n5. Click 'Stay Signed In' (ASSUMPTION: triggers silent refresh flow); expect a background POST /v2/auth/token/refresh 200 and cookies rotated, session timer reset; CSRF may remain valid or be transparently refreshed.\n6. Immediately POST /v2/applications/{application_id}/financials with X-App-Session and X-CSRF-Token CSRF-A; expect 200 or, if CSRF rotated, 403 prompting auto-fetch of new CSRF; fetch new CSRF if needed and re-submit successfully.\n7. Verify draft preservation: reload the page; confirm Step 1 non-sensitive fields restored; confirm ssn_last4 or sensitive data is absent in localStorage.\n8. Idle again for 14 minutes, then interact within the warning window by clicking any UI control (e.g., open notifications); confirm the timer resets (no auto-logout at 15 minutes).\n9. Negative path: Now ignore the warning modal when it appears next cycle; continue idling past 15 minutes; verify auto-logout occurs and protected API calls return 401.\n10. Re-login with MFA; confirm previous draft remains accessible; ensure cookies are Secure, HttpOnly, SameSite=Strict and no tokens in storage.", + "expectedResult": "'Stay Signed In' prevents auto-logout by extending session; tokens rotate as needed; CSRF is managed per-session; application draft remains intact and non-sensitive; ignoring the warning results in auto-logout and 401 on protected endpoints.", + "apiPath": "/v2/applications/start, /v2/applications/{application_id}/financials, /v2/auth/token/refresh, /v2/auth/csrf", + "httpMethod": "POST, POST, POST, GET", + "endpointGroup": "Applications, Auth", + "workflow": "User Authentication & Session Management", + "businessRuleIds": "NFR-06, NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for mutating calls", + "mfaRequired": "true at initial login", + "rateLimitBucket": "standard", + "inputFields": "employment_status,employer_name,gross_annual_income,monthly_rent,existing_debt_payments,sin_consent,X-App-Session", + "validationRules": "Session timeout at 15 minutes; warning at 13 minutes; Steps 2 and 3 require valid session_token; CSRF required for POST.", + "errorCodesCovered": "SESSION_EXPIRED, CSRF_MISSING, CSRF_INVALID, UNAUTHORIZED", + "stateTransitions": "Session: active->warning->extended->active or active->warning->expired", + "dataMaskingChecks": "No sensitive data (ssn_last4, PAN, tokens) in localStorage/DOM; masked PAN only when displayed", + "auditTrailChecks": "ASSUMPTION: Session extension and expiration are logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "full_legal_name,email,phone_number,residential_address", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: 'Stay Signed In' triggers token refresh and resets inactivity counter; CSRF remains valid or is reissued transparently.", + "testData": "Valid Step 1 personal info and Step 2 financials; device_id UUIDv4", + "cleanupSteps": "Logout; clear cookies; remove draft via test harness", + "dependencies": "Frontend implements 13-minute warning modal; autosave enabled; refresh endpoint available" + }, + { + "type": "functional", + "title": "Transaction Currency and Field Validation: Invalid currency_code, Negative/Zero FX Rate, Decimal Scale, Invalid Category Filter", + "description": "Validate currency and amount constraints for transactions: invalid ISO currency_code rejected, exchange_rate must be positive with Decimal(8,6), transaction_amount must have scale <= 2, and invalid category filter rejected.", + "testId": "TC-TXN-CURRENCY-VALID-028", + "testDescription": "Covers negative and non-ISO currency codes, zero/negative exchange_rate, too many decimals in transaction_amount, and invalid category enumeration in list API; includes a success case at precision boundaries.", + "prerequisites": "Active account_id with available_credit >= $50.00; authenticated session with CSRF; merchant test IDs; TLS 1.3.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/summary to confirm Active status and available_credit baseline.\n2. POST /v2/accounts/{account_id}/transactions with currency_code 'USDX' (invalid), transaction_amount 10.00, exchange_rate 1.250000, MCC 3000; expect 400 validation error for currency_code.\n3. POST with currency_code USD but exchange_rate 0.000000; expect 400 validation error (FX rate must be positive) (ASSUMPTION: rule enforced).\n4. POST with currency_code USD and exchange_rate -1.230000; expect 400 validation error for negative rate.\n5. POST with CAD (omit currency_code) but transaction_amount 10.999 (more than 2 decimals); expect 400 or 422 validation error for Decimal(10,2) scale.\n6. POST a valid FX transaction at precision boundary: currency_code EUR, transaction_amount 2.50, exchange_rate 0.999999 (Decimal(8,6) max scale), mcc_code 3000, merchant_name 'EuroTravel', merchant_id 'ET001', transaction_type PURCHASE; expect 200 approval and correct foreign_fee_amount = (2.50×0.999999×0.03) rounded to 2 decimals; Total_CAD = (2.50×0.999999)×1.03.\n7. Verify response includes transaction_id, available_credit decreased by Total_CAD; amounts rounded to two decimals, fee itemized.\n8. GET /v2/accounts/{account_id}/transactions with category 'GIFT' (invalid); expect 400 validation error for category.\n9. GET /v2/accounts/{account_id}/transactions with valid category PURCHASE and page=1, per_page=25; expect 200 with the valid FX transaction present.\n10. Attempt GET transactions for another account_id not owned; expect 403 FORBIDDEN owner-only access.\n11. Confirm all transaction-related responses and lists show masked PAN and no PII leakage.", + "expectedResult": "Invalid currency_code, zero/negative exchange_rate, and transaction_amount with >2 decimals are rejected. Valid FX at max precision is accepted with correct 3% fee math and rounded amounts. Invalid category filter is rejected. Owner-only access enforced with masked PAN.", + "apiPath": "/v2/accounts/{account_id}/transactions (POST), /v2/accounts/{account_id}/transactions (GET), /v2/accounts/{account_id}/summary (GET)", + "httpMethod": "POST, GET, GET", + "endpointGroup": "Transactions, Accounts", + "workflow": "Transaction Processing API", + "businessRuleIds": "REQ-006, NFR-05", + "calculationFormula": "Total_CAD = (transaction_amount × exchange_rate) × 1.03; foreign_fee_amount = (transaction_amount × exchange_rate) × 0.03; monetary outputs rounded to 2 decimals.", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for POST", + "mfaRequired": "false", + "rateLimitBucket": "transactions standard", + "inputFields": "transaction_amount,merchant_name,merchant_id,mcc_code,currency_code,exchange_rate,transaction_type,category,page,per_page", + "validationRules": "currency_code must be valid ISO 4217; exchange_rate Decimal(8,6) > 0 when currency!=CAD; transaction_amount Decimal(10,2) scale<=2 and > 0; category must be one of allowed enums.", + "errorCodesCovered": "400 field validation error, FORBIDDEN", + "stateTransitions": "Available_credit decremented on valid approval", + "dataMaskingChecks": "Masked PAN only; no sensitive PII in responses", + "auditTrailChecks": "ASSUMPTION: Approvals logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "None beyond account ownership", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: API validates currency_code strictly and enforces positive exchange_rate.", + "testData": "Invalid currency_code 'USDX'; valid EUR with exchange_rate 0.999999; CAD transaction_amount 10.999 for negative test", + "cleanupSteps": "None; keep valid FX transaction for billing validation", + "dependencies": "Sufficient available_credit; CSRF token present" + }, + { + "type": "functional", + "title": "Payments CUSTOM Boundary: Min $1.00, Scale Enforcement, Max = total_balance, Timezone Scheduling", + "description": "Validate payment_type CUSTOM rules including minimum/maximum amounts, decimal scale enforcement, and scheduled_date timezone boundary; ensure CSRF enforcement and owner-only access.", + "testId": "TC-PAYMENT-CUSTOM-BOUNDARY-029", + "testDescription": "Covers CUSTOM payment acceptance at exactly $1.00, rejection for >2 decimal places, rejection when exceeding total_balance, acceptance when equal to total_balance, and scheduled_date handling around UTC midnight.", + "prerequisites": "Active account with known total_balance and minimum_payment_due; at least one active bank_account_id linked; authenticated session with CSRF; TLS 1.3.", + "stepsToPerform": "1. GET /v2/accounts/{account_id}/statements/{statement_id} (latest) to capture total_balance and minimum_payment_due.\n2. GET linked bank accounts via ASSUMPTION helper or known bank_account_id; ensure bank_account_id is active.\n3. POST /v2/accounts/{account_id}/payments with payment_type CUSTOM, payment_amount 1.00, bank_account_id active, no scheduled_date; include X-CSRF-Token; expect 200 payment_id and scheduled_date today.\n4. POST a CUSTOM payment with payment_amount 0.999 (scale >2); expect 400 validation error for Decimal(10,2) scale; no balance change.\n5. POST a CUSTOM payment with payment_amount total_balance + 0.01; expect 400 validation error due to max = total_balance (ASSUMPTION: error code ABOVE_MAX or generic validation); no balance change.\n6. POST a CUSTOM payment with payment_amount exactly equal to current total_balance; expect 200 payment_id and new_balance_estimate near $0.00.\n7. Schedule a CUSTOM payment for the next day around UTC boundary: set scheduled_date to tomorrow's date (ISO 8601), verify 200 queued response and that scheduled_date is stored in UTC; ensure past date submission returns 400 INVALID_SCHEDULED_DATE.\n8. Attempt POST /v2/accounts/{account_id}/payments without X-CSRF-Token; expect 403 CSRF_MISSING and no payment created.\n9. Attempt POST payment for another user's account_id; expect 403 FORBIDDEN.\n10. Confirm cookies flags remain Secure, HttpOnly, SameSite=Strict; ensure no PAN or bank details leak in responses; only identifiers are shown.", + "expectedResult": "CUSTOM payments accept $1.00 and amounts up to total_balance, reject >2 decimal places and above-total_balance amounts, enforce CSRF and owner-only access, and accept future scheduled_date while rejecting past dates.", + "apiPath": "/v2/accounts/{account_id}/payments, /v2/accounts/{account_id}/statements/{statement_id}", + "httpMethod": "POST, GET", + "endpointGroup": "Payments, Billing", + "workflow": "Billing & Financial Logic", + "businessRuleIds": "REQ-015 (statement reference), NFR-05", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true", + "mfaRequired": "false", + "rateLimitBucket": "payments standard", + "inputFields": "payment_amount,payment_type,bank_account_id,scheduled_date,account_id,statement_id", + "validationRules": "payment_amount Decimal(10,2) >= 1.00 and <= total_balance; scheduled_date must be future date; CSRF required; owner-only.", + "errorCodesCovered": "BELOW_MINIMUM (contextual), CSRF_MISSING, FORBIDDEN, INVALID_SCHEDULED_DATE, 400 generic validation", + "stateTransitions": "Balance reduced upon accepted immediate or scheduled payment posting (queued state for scheduled)", + "dataMaskingChecks": "No PAN or bank account sensitive details returned; masked or identifiers only", + "auditTrailChecks": "ASSUMPTION: Payment creation logged with user_id/session_id/ip/timestamp_utc", + "piiFields": "bank_account_id (identifier only)", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security", + "assumptions": "ASSUMPTION: Exceeding total_balance returns a 400 validation error; scheduled payments stored in UTC.", + "testData": "Known total_balance; active bank_account_id; minimum_payment_due value from latest statement", + "cleanupSteps": "If needed, cancel or allow scheduled payment to process in non-prod; reconcile test ledger", + "dependencies": "Linked bank account present; statement available; CSRF endpoint available" + }, + { + "type": "functional", + "title": "Right to Rescind Security Sweep: Post-Delete Access Denials, WebSocket Closure, Idempotency", + "description": "Validate that after a successful DELETE within the 14-day window, all subsequent access to account resources is denied, realtime streams are closed, and repeated DELETEs are handled idempotently.", + "testId": "TC-RESCIND-SECURITY-031", + "testDescription": "Extends REQ-016 by verifying cross-surface security behaviors post-rescind: protected APIs return 403/404, transactions and payments are blocked, WebSocket subscriptions are invalidated, notifications suppressed, and duplicate DELETE behaves safely.", + "prerequisites": "Newly issued account within 14 days; authenticated session with CSRF token; WebSocket JWT available; no critical pending payments.", + "stepsToPerform": "1. Confirm account issuance date < 14 days; GET /v2/accounts/{account_id}/summary returns 200 with Active status.\n2. Open WebSocket wss://realtime.aegiscard.com/v2/stream with valid JWT; subscribe to account:{account_id}:transactions; verify subscription ack.\n3. Initiate a small CAD PURCHASE via POST /v2/accounts/{account_id}/transactions; expect 200 and one realtime event; confirm masked PAN in event.\n4. Call DELETE /v2/accounts/{id} with X-CSRF-Token to exercise Right to Rescind; expect 200 success and audit reference (ASSUMPTION); account transitions to Closed.\n5. Immediately attempt GET /v2/accounts/{account_id}/summary; expect 404 NOT_FOUND or 403 FORBIDDEN per design; no data exposure.\n6. Attempt POST /v2/accounts/{account_id}/transactions; expect 403 FORBIDDEN or CARD_INACTIVE equivalent; confirm no transaction created.\n7. Attempt POST /v2/accounts/{account_id}/payments; expect 403 FORBIDDEN; no payment created.\n8. Verify the existing WebSocket receives a termination/unauthorized event or is closed by server; further events should not be delivered for the closed account.\n9. POST /v2/notifications/webhook targeting the rescinded account with alert_type STATEMENT_READY; expect either 404 or safe no-op (200 queued ignored) (ASSUMPTION: suppression); confirm no in-portal alert appears for the user.\n10. Attempt DELETE /v2/accounts/{id} again; expect idempotent response such as 404 NOT_FOUND or 409 ALREADY_CLOSED (ASSUMPTION); state unchanged.\n11. Ensure no PAN or PII leaked in any error responses and cookies remain Secure, HttpOnly, SameSite=Strict.", + "expectedResult": "After rescind, all account-bound APIs reject access, realtime subscription is invalidated, webhook deliveries are suppressed or no-op for the closed account, and repeated DELETE is idempotently handled. No PII leaks occur.", + "apiPath": "/v2/accounts/{account_id}/summary, /v2/accounts/{account_id}/transactions, /v2/accounts/{account_id}/payments, /v2/accounts/{id}, /v2/notifications/webhook, wss://realtime.aegiscard.com/v2/stream", + "httpMethod": "GET, POST, POST, DELETE, POST, WebSocket", + "endpointGroup": "Account Lifecycle, Transactions, Payments, Notifications, Realtime", + "workflow": "Right to Rescind, Security & Compliance Controls", + "businessRuleIds": "REQ-016, NFR-05, REQ-014", + "calculationFormula": "N/A", + "rolesCovered": "Cardholder", + "authRequired": "true", + "csrfRequired": "true for DELETE/POST", + "mfaRequired": "false", + "rateLimitBucket": "standard", + "inputFields": "id (account id),alert_type,channel,severity,idempotency_key", + "validationRules": "DELETE allowed within 14 days only; post-delete access denied; WebSocket must enforce ownership and account state.", + "errorCodesCovered": "FORBIDDEN, NOT_FOUND, ALREADY_CLOSED (ASSUMPTION)", + "stateTransitions": "Account: Active->Closed (rescinded); Socket: connected->terminated", + "dataMaskingChecks": "Masked PAN in any residual events; no PII in errors", + "auditTrailChecks": "Rescind action logged with user_id/session_id/ip/timestamp_utc; attempted post-delete actions also logged", + "piiFields": "None returned post-delete", + "riskLevel": "High", + "regulatoryTags": "PCI-DSS L1, Security, CSRF", + "assumptions": "ASSUMPTION: WebSocket disconnects on account closure; webhook suppression model returns safe status; duplicate DELETE handled idempotently.", + "testData": "account_id issued <14 days; idempotency_key a new UUID for webhook call", + "cleanupSteps": "None (account remains closed in non-prod); ensure no orphaned scheduled payments exist", + "dependencies": "WebSocket service operational; CSRF issuance; notifications engine reachable" + } +] \ No newline at end of file diff --git a/functional_tests/functional-test-aegis/functional-test-aegis.xlsx b/functional_tests/functional-test-aegis/functional-test-aegis.xlsx new file mode 100644 index 0000000..1b19c0d Binary files /dev/null and b/functional_tests/functional-test-aegis/functional-test-aegis.xlsx differ