mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
Every spec but four went red when sign-in became IAM's job: they submitted a password form that no longer exists. Mocking around that would have left the one thing worth proving untested, so the suite now runs the real OIDC round trip against a stand-in identity provider. e2e/iam-stub.mjs is that provider. It generates an RS256 key at boot and publishes the public half at the JWKS URL bankd reads, so the token the browser comes back with is one bankd actually verifies — not one the test asserted about. Pointing bankd at it is the whole of the wiring: the app never addresses IAM directly, it proxies /v1/iam/*, so IAM_ENDPOINT moves the entire flow. The browser runs exactly the redirect it runs in production. That flow only completes with the proxy fix released alongside this: authorize answers 302, and the proxy used to follow it server-side, so sign-in died at that hop with "iam unreachable". Two things in the app had to move with it. The demo account can no longer be seeded at boot — its owner is IAM's subject, which nothing knows until someone signs in — so overview opens it on first sight through requireAccount. Reporting onboarded:false there instead left the shell in its pre-onboarding state, no nav and nothing to click, for a customer who had just signed in successfully. The specs also carried figures in dollars where the screen now shows the vault's own token, and a fixed borrow of 100 that was under the limit in cents and over it in LUX. They read the headroom the screen states and borrow a slice of it, so they hold whichever unit the page is in and whatever the specs before them left on the ledger. 34 of 34 pass. Co-authored-by: Hanzo Dev <[email protected]>
71 lines
3.2 KiB
TypeScript
71 lines
3.2 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
|
import { signIn } from './demo'
|
|
|
|
// The app shell contract: unknown authenticated routes stay in the product,
|
|
// every screen is reachable on mobile, a freshly issued card is readable once,
|
|
// and the IAM redirect is well-formed. These are the gaps the visual sweep found.
|
|
|
|
test('an unknown /app route stays in the shell, not the marketing site', async ({ page }) => {
|
|
await signIn(page)
|
|
await page.goto('/app/nowhere-at-all')
|
|
// URL is preserved (no rewrite to the public landing) and the in-shell 404 shows.
|
|
await expect(page).toHaveURL(/\/app\/nowhere-at-all$/)
|
|
await expect(page.getByText('Page not found')).toBeVisible()
|
|
// The nav rail is still there — we're inside the product, not the hero page.
|
|
await expect(page.getByRole('link', { name: 'Wallet' }).first()).toBeVisible()
|
|
await expect(page.getByRole('heading', { name: /Banking without borders/i })).toHaveCount(0)
|
|
})
|
|
|
|
test('on mobile, More reaches Accounts and Activity', async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
await signIn(page)
|
|
await page.getByRole('button', { name: 'More' }).click()
|
|
await page.getByRole('link', { name: 'Accounts' }).click()
|
|
await expect(page).toHaveURL(/\/app\/accounts$/)
|
|
})
|
|
|
|
// Navigation is a landmark, and a screen with more than one names them apart.
|
|
test('every set of links is a named navigation landmark', async ({ page }) => {
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
await signIn(page)
|
|
|
|
const tabs = page.getByRole('navigation', { name: 'Primary' })
|
|
await expect(tabs).toBeVisible()
|
|
await expect(tabs.getByRole('link', { name: 'Wallet' })).toBeVisible()
|
|
|
|
await page.getByRole('button', { name: 'More' }).click()
|
|
await expect(page.getByRole('navigation', { name: 'More' })).toBeVisible()
|
|
|
|
await page.setViewportSize({ width: 1280, height: 900 })
|
|
await expect(page.getByRole('navigation', { name: 'Sections' })).toBeVisible()
|
|
|
|
await page.goto('/')
|
|
await expect(page.getByRole('navigation', { name: 'Main' })).toBeVisible()
|
|
})
|
|
|
|
test('a freshly issued card reveals its full number and CVV once', async ({ page }) => {
|
|
await signIn(page)
|
|
await page.goto('/app/cards')
|
|
await page.getByRole('button', { name: /New card|Issue a card/i }).first().click()
|
|
// The reveal modal appears and shows a full 16-digit PAN (4 groups of 4) and
|
|
// a CVV — not the masked bullets the card list carries.
|
|
await expect(page.getByText('Your new card is ready')).toBeVisible()
|
|
await expect(page.getByText('CVV').first()).toBeVisible()
|
|
const modalText = await page.getByText('Your new card is ready').locator('xpath=ancestor::*[3]').innerText()
|
|
expect(modalText).toMatch(/\d{4} \d{4} \d{4} \d{4}/)
|
|
expect(modalText).not.toMatch(/•/)
|
|
})
|
|
|
|
test('the IAM sign-in redirect carries a single /v1/iam prefix', async ({ page }) => {
|
|
await page.goto('/login')
|
|
// The SSO button starts an OIDC redirect; capture where it points.
|
|
const [nav] = await Promise.all([
|
|
page.waitForRequest((r) => r.url().includes('/oauth/authorize'), { timeout: 8000 }).catch(() => null),
|
|
page.getByRole('button', { name: /Sign in with Lux ID/i }).click(),
|
|
])
|
|
if (nav) {
|
|
expect(nav.url()).toContain('/v1/iam/oauth/authorize')
|
|
expect(nav.url()).not.toContain('/v1/iam/v1/iam/')
|
|
}
|
|
})
|