codeburn/packages/cli/tests/sync-headless-e2e.test.ts

143 lines
5.2 KiB
TypeScript

/**
* E2E test: codeburn sync setup with headless browser against real Cognito.
*
* This test exercises the FULL PKCE flow:
* 1. Starts the callback server (simulating `codeburn sync setup`)
* 2. Builds the auth URL with PKCE challenge
* 3. Opens a headless Chromium to the Cognito Hosted UI
* 4. Fills the login form with test credentials
* 5. Submits → Cognito redirects to localhost callback
* 6. Callback server receives code + state
* 7. Exchanges code for tokens
*
* Requirements:
* - Playwright + Chromium installed (`npx playwright install chromium`)
* - Real Cognito endpoint deployed (CodeburnSyncBackend stack)
* - Test user created in the pool
*
* Run: npx vitest run tests/sync-headless-e2e.test.ts
* Skip in CI without infra: set SKIP_HEADLESS_E2E=1
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { chromium, type Browser, type Page } from 'playwright'
import { fetchDiscoveryDoc } from '../src/sync/discovery.js'
import {
fetchOidcConfig,
generatePkce,
buildAuthUrl,
resolveScopes,
startCallbackServer,
exchangeCode,
} from '../src/sync/auth.js'
import { randomBytes } from 'crypto'
// --- Configuration (from deployed stack outputs) ---
const BASE_URL = process.env.CODEBURN_SYNC_URL
const TEST_EMAIL = process.env.CODEBURN_SYNC_EMAIL
const TEST_PASSWORD = process.env.CODEBURN_SYNC_PASSWORD
// Only runs when ALL three env vars are set. Developer-only test.
const SKIP = !BASE_URL || !TEST_EMAIL || !TEST_PASSWORD
describe.skipIf(SKIP)('sync setup — headless browser PKCE flow', () => {
let browser: Browser
beforeAll(async () => {
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
})
})
afterAll(async () => {
await browser?.close()
})
it('full PKCE flow: callback server → Cognito login → token exchange', async () => {
// 1. Fetch discovery + OIDC config
const discovery = await fetchDiscoveryDoc(BASE_URL)
const oidc = await fetchOidcConfig(discovery.issuer)
const scopes = resolveScopes(discovery.scopes, oidc.scopes_supported)
// 2. PKCE + state
const pkce = generatePkce()
const state = randomBytes(16).toString('hex')
// 3. Start callback server
const { promise: callbackPromise, ready } = startCallbackServer(state, 30000)
const port = await ready
const redirectUri = `http://127.0.0.1:${port}/callback`
// 4. Build auth URL
const authUrl = buildAuthUrl({
authorization_endpoint: oidc.authorization_endpoint,
client_id: discovery.client_id,
redirect_uri: redirectUri,
scopes,
state,
pkce,
})
// 5. Open headless browser to Cognito Hosted UI
const page: Page = await browser.newPage()
try {
await page.goto(authUrl, { waitUntil: 'networkidle' })
// 6. Fill login form
// Cognito Hosted UI renders two tabbed forms (Sign In + Sign Up).
// The inputs exist but may not be CSS-visible. Use JS to fill the
// sign-in form directly, targeting inputs within the form that has
// the signInSubmitButton.
await page.evaluate((creds) => {
const forms = document.querySelectorAll('form')
for (const form of forms) {
if (!form.querySelector('input[name="signInSubmitButton"]')) continue
const username = form.querySelector('input[name="username"]') as HTMLInputElement
const password = form.querySelector('input[name="password"]') as HTMLInputElement
if (username) { username.value = creds.email; username.dispatchEvent(new Event('input', { bubbles: true })) }
if (password) { password.value = creds.password; password.dispatchEvent(new Event('input', { bubbles: true })) }
}
}, { email: TEST_EMAIL, password: TEST_PASSWORD })
// 7. Submit the form via JS
await page.evaluate(() => {
const btn = document.querySelector('input[name="signInSubmitButton"]') as HTMLInputElement
if (btn) btn.click()
})
// 8. Wait for redirect to our callback server
// Cognito will redirect to http://127.0.0.1:{port}/callback?code=...&state=...
// The page will load our callback server's "Login successful" response
await page.waitForURL(`http://127.0.0.1:${port}/callback*`, { timeout: 15000 })
// 9. Callback server should have received the code
const result = await callbackPromise
expect(result.code).toBeTruthy()
expect(result.code.length).toBeGreaterThan(10)
// 10. Exchange code for tokens
const tokens = await exchangeCode(
oidc.token_endpoint,
result.code,
pkce.code_verifier,
redirectUri,
discovery.client_id,
)
expect(tokens.access_token).toBeTruthy()
expect(tokens.refresh_token).toBeTruthy()
expect(tokens.token_type).toBe('Bearer')
expect(tokens.expires_in).toBeGreaterThan(0)
console.log('✅ Full PKCE flow completed successfully!')
console.log(` Access token: ${tokens.access_token.slice(0, 30)}...`)
console.log(` Refresh token: ${tokens.refresh_token!.slice(0, 30)}...`)
console.log(` Expires in: ${tokens.expires_in}s`)
} finally {
await page.close()
}
}, 60000) // 60s timeout for the full browser flow
})