Functional test generated by RoostGPT#72
Conversation
Using AI Model gpt-5
There was a problem hiding this comment.
Pull request overview
Adds a new Roost-generated “functional-test-aegis” bundle under functional_tests/, describing an Aegis Card portal end-to-end journey plus a broad set of security/compliance/transactions/billing functional scenarios.
Changes:
- Added new test artifacts for
functional-test-aegis(JSON spec, Gherkin feature, CSV index, XLSX/DOCX outputs, Roost metadata). - Updated
functional_tests/README.mdto register the new test run and link to its outputs.
Reviewed changes
Copilot reviewed 4 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| functional_tests/functional-test-aegis/functional-test-aegis.json | Large JSON test catalog describing scenarios, steps, expected results, and assumptions. |
| functional_tests/functional-test-aegis/functional-test-aegis.feature | Gherkin-style feature file mirroring the JSON scenarios for human-readable execution. |
| functional_tests/functional-test-aegis/functional-test-aegis.csv | Flat list of scenario titles for quick indexing. |
| functional_tests/functional-test-aegis/functional-test-aegis.xlsx | Spreadsheet export of the test cases. |
| functional_tests/functional-test-aegis/functional-test-aegis.docx | Document export of the test cases. |
| functional_tests/functional-test-aegis/.roost/roost_metadata.json | Roost metadata capturing inputs and provenance. |
| functional_tests/README.md | Appends an entry for this test run and links to generated outputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| { | ||
| "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.", |
There was a problem hiding this comment.
The testDescription/description mentions “OAuth2 login”, but the described flow here uses /v2/auth/login rather than an OAuth2 auth-code/PKCE exchange. Either update the description to match the actual steps, or incorporate the OAuth2 flow (which you already cover in a separate PKCE test case).
| "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.", | |
| "description": "Covers full new-user onboarding with regulatory controls: registration validations, email verification, login with MFA/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-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.", |
There was a problem hiding this comment.
This test case embeds a full PAN-like string (4111 1111 1111 1111) inside stepsToPerform. Even if it’s a test number, storing full PAN patterns in-repo can trip PCI/secret scanning and is often disallowed. Prefer a non-PAN placeholder token and keep the masking assertion (e.g., “system must mask any 16-digit sequences”) without including the raw number.
| "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.", | |
| "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 [PAN_16_DIGITS] at Merchant X', idempotency_key UUID3; expect 200; verify backend masks any PAN-like 16-digit sequence in stored/displayed message per REQ-014, no unmasked 16-digit sequence appears in payload or UI, 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.", |
| "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.", |
There was a problem hiding this comment.
This section repeats the full PAN-like string (4111 1111 1111 1111) (and also includes a regex intended to find PANs). If you need to test masking behavior, consider using a synthetic placeholder (e.g., PAN_TEST_16DIGITS) and/or a generated value at runtime rather than committing a literal 16-digit sequence in the JSON.
| 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' |
There was a problem hiding this comment.
The functional_tests/*.feature files consistently parameterize the API host via an environment variable (e.g., functional_tests/test-new-hello/test-new-hello.feature:5, functional_tests/ZBIO-5136/ZBIO-5136.feature:5, functional_tests/ZBIO-5139/ZBIO-5139.feature:6). Hardcoding the API/portal/WebSocket URLs here makes the test artifact less portable and increases the risk of someone running it against the wrong environment. Consider switching these to env-var driven values (and avoid embedding a /v2 suffix in the base so paths stay consistent).
| 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' | |
| Given the API base URL is '${API_BASE_URL}' | |
| And the portal URL is '${PORTAL_URL}' | |
| And the realtime WebSocket URL is '${REALTIME_WS_URL}' |
| 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/<id>' DELETE to exercise Right to Rescind within 14 days with 'X-CSRF-Token' |
There was a problem hiding this comment.
This step mixes HTTP methods: When I POST '/accounts/<id>' DELETE .... That’s internally inconsistent and will likely break any downstream parser/runner. Update it to a single, correct method (e.g., When I DELETE '/accounts/<id>' ...).
| When I POST '/accounts/<id>' DELETE to exercise Right to Rescind within 14 days with 'X-CSRF-Token' | |
| When I DELETE '/accounts/<id>' to exercise Right to Rescind within 14 days with 'X-CSRF-Token' |
| | 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 | <tb+0.01>| 11111111-2222-3333-4444-555555555555 | null | 400 | ABOVE_MAX | | ||
| | X-CSRF-Token | <tb> | 11111111-2222-3333-4444-555555555555 | "tomorrow" | 200 | | | ||
| | (missing) | 5.00 | 11111111-2222-3333-4444-555555555555 | null | 403 | CSRF_MISSING | | ||
|
|
There was a problem hiding this comment.
In this Scenario Outline, the Examples table uses (missing) as a value for <csrf_header>. Since the step says with headers '<csrf_header>', this can be interpreted as literally adding a header named (missing) rather than omitting CSRF. Consider splitting the missing-CSRF negative into its own scenario/step, or use a dedicated mechanism your runner understands for “omit this header”.
| | 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 | <tb+0.01>| 11111111-2222-3333-4444-555555555555 | null | 400 | ABOVE_MAX | | |
| | X-CSRF-Token | <tb> | 11111111-2222-3333-4444-555555555555 | "tomorrow" | 200 | | | |
| | (missing) | 5.00 | 11111111-2222-3333-4444-555555555555 | null | 403 | CSRF_MISSING | | |
| | 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 | <tb+0.01>| 11111111-2222-3333-4444-555555555555 | null | 400 | ABOVE_MAX | | |
| | X-CSRF-Token | <tb> | 11111111-2222-3333-4444-555555555555 | "tomorrow" | 200 | | | |
| @api @payments | |
| Scenario: Payments CUSTOM rejects requests with missing CSRF header | |
| Given total_balance and an active bank_account_id are known | |
| When I POST '/accounts/<account_id>/payments' without the CSRF header and with JSON payload | |
| """ | |
| {"payment_type":"CUSTOM","payment_amount":5.00,"bank_account_id":"11111111-2222-3333-4444-555555555555","scheduled_date":null} | |
| """ | |
| Then the response status should be 403 | |
| And the error code should be 'CSRF_MISSING' |
| When I POST a FRAUD_FLAG IN_APP alert containing a PAN-like string | ||
| """ | ||
| {"account_id":"<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":"<uuid5>"} | ||
| """ | ||
| Then the response status should be 200 | ||
| And stored/rendered message masks to '**** **** **** 1111' |
There was a problem hiding this comment.
This payload includes a full PAN-like test number (4111 1111 1111 1111). Even if it’s not a real card, committing full PAN patterns can trigger secret/PCI scanners and can violate data-handling policies. Prefer a non-PAN placeholder (e.g., PAN_LIKE_STRING) or a partially masked string, and describe the masking expectation without embedding a 16-digit sequence.
| When I POST a FRAUD_FLAG IN_APP alert containing a PAN-like string | |
| """ | |
| {"account_id":"<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":"<uuid5>"} | |
| """ | |
| Then the response status should be 200 | |
| And stored/rendered message masks to '**** **** **** 1111' | |
| When I POST a FRAUD_FLAG IN_APP alert containing a PAN placeholder | |
| """ | |
| {"account_id":"<account_id>","alert_type":"FRAUD_FLAG","channel":"IN_APP","message_body":"Suspicious charge on card PAN_LIKE_STRING at Merchant X","severity":"CRITICAL","idempotency_key":"<uuid5>"} | |
| """ | |
| Then the response status should be 200 | |
| And stored/rendered message masks the card reference |
RoostGPT Logs