mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-02 04:45:21 +00:00
143 lines
5.2 KiB
TypeScript
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
|
|
})
|