mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-12 02:20:12 +08:00
* chore: upgrade dependencies - dompurify 3.3.1 → 3.3.2 - naive-ui 2.43.2 → 2.44.0 - vue-i18n 11.2.8 → 11.3.0 - @cloudflare/workers-types 4.20260305.1 → 4.20260307.1 - @types/node 25.3.3 → 25.3.5 - wrangler 4.70.0 → 4.71.0 (all subprojects) * feat: upgrade @simplewebauthn packages from v10 to v13 Breaking changes addressed: - [v11] startRegistration/startAuthentication now take object param - [v11] registrationInfo.credential replaces flat destructuring - [v11] authenticator param renamed to credential in verifyAuthenticationResponse - [v13] @simplewebauthn/types removed, types imported from @simplewebauthn/server Packages: - @simplewebauthn/server: 10.0.1 → 13.2.3 - @simplewebauthn/browser: 10.0.0 → 13.2.2 - @simplewebauthn/types: removed (deprecated) * test: add passkey API E2E tests - User registration and login flow - register_request/authenticate_request return valid WebAuthn options - authenticate_response with invalid credential returns 404 - register_response with invalid credential returns error - Passkey list empty for new user - Rename/delete operations with validation * fix: use UI login instead of localStorage injection in browser passkey test The localStorage approach doesn't work with VueUse's useStorage because it doesn't detect external changes during page navigation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: hash password before registration to match frontend login behavior The frontend hashes passwords with SHA-256 before sending to the API. Registration via API must use the same hashed password so that UI login matches the stored value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: allow crypto.subtle in Docker browser tests The frontend uses crypto.subtle for password hashing, which requires a secure context (HTTPS or localhost). In Docker, the frontend runs at http://frontend:5173 which is not a secure context. Add Chromium flag to treat this origin as secure. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: serve frontend over HTTPS in Docker for WebAuthn secure context WebAuthn (navigator.credentials) and crypto.subtle both require a secure context (HTTPS or localhost). The Docker frontend was serving over HTTP, making passkey operations impossible. Changes: - Generate self-signed cert in Dockerfile.frontend - Configure Vite to serve over HTTPS - Update FRONTEND_URL to https:// - Add ignoreHTTPSErrors to Playwright browser config - Use localStorage injection for passkey test login Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add Vite proxy to avoid mixed-content blocking in HTTPS Docker frontend HTTPS pages cannot make HTTP API requests (mixed content). Add a Vite proxy for all API paths so the browser makes same-origin HTTPS requests, which Vite proxies to the HTTP worker server-to-server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: store userJwt without JSON.stringify in localStorage VueUse's useStorage with a string default uses raw string serialization (no JSON wrapping). Using JSON.stringify added double quotes around the JWT token, causing 401 Unauthorized from the worker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: clean up passkey API test per review feedback Remove unused variables and rename test to match actual behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
163 lines
5.5 KiB
TypeScript
163 lines
5.5 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import type { APIRequestContext } from '@playwright/test';
|
|
import { WORKER_URL } from '../../fixtures/test-helpers';
|
|
|
|
const TEST_USER_EMAIL = `passkey-e2e-${Date.now()}@test.example.com`;
|
|
const TEST_USER_PASSWORD = 'test-password-123';
|
|
|
|
/**
|
|
* Enable user registration via admin API, register a user, and login to get JWT.
|
|
*/
|
|
async function createTestUser(request: APIRequestContext): Promise<string> {
|
|
// Enable user registration (KV setting)
|
|
const enableRes = await request.post(`${WORKER_URL}/admin/user_settings`, {
|
|
data: {
|
|
enable: true,
|
|
enableMailVerify: false,
|
|
},
|
|
});
|
|
expect(enableRes.ok()).toBe(true);
|
|
|
|
// Register user
|
|
const registerRes = await request.post(`${WORKER_URL}/user_api/register`, {
|
|
data: { email: TEST_USER_EMAIL, password: TEST_USER_PASSWORD },
|
|
});
|
|
expect(registerRes.ok()).toBe(true);
|
|
|
|
// Login to get JWT
|
|
const loginRes = await request.post(`${WORKER_URL}/user_api/login`, {
|
|
data: { email: TEST_USER_EMAIL, password: TEST_USER_PASSWORD },
|
|
});
|
|
expect(loginRes.ok()).toBe(true);
|
|
const { jwt } = await loginRes.json();
|
|
expect(jwt).toBeTruthy();
|
|
return jwt;
|
|
}
|
|
|
|
test.describe('Passkey API', () => {
|
|
let userJwt: string;
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
userJwt = await createTestUser(request);
|
|
});
|
|
|
|
test('register_request returns valid WebAuthn options', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/register_request`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: { domain: 'localhost' },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const options = await res.json();
|
|
|
|
// Verify WebAuthn registration options structure
|
|
expect(options.rp).toBeDefined();
|
|
expect(options.rp.id).toBe('localhost');
|
|
expect(options.user).toBeDefined();
|
|
expect(options.user.name).toBe(TEST_USER_EMAIL);
|
|
expect(options.challenge).toBeTruthy();
|
|
expect(options.pubKeyCredParams).toBeInstanceOf(Array);
|
|
expect(options.pubKeyCredParams.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('authenticate_request returns valid WebAuthn options', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/authenticate_request`, {
|
|
data: { domain: 'localhost' },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const options = await res.json();
|
|
|
|
// Verify WebAuthn authentication options structure
|
|
expect(options.challenge).toBeTruthy();
|
|
expect(options.rpId).toBe('localhost');
|
|
expect(options.allowCredentials).toBeInstanceOf(Array);
|
|
});
|
|
|
|
test('authenticate_response with invalid credential returns error', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/authenticate_response`, {
|
|
data: {
|
|
domain: 'localhost',
|
|
origin: 'http://localhost',
|
|
credential: { id: 'nonexistent-passkey-id' },
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(false);
|
|
expect(res.status()).toBe(404);
|
|
});
|
|
|
|
test('passkey list is empty for new user', async ({ request }) => {
|
|
const res = await request.get(`${WORKER_URL}/user_api/passkey`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const passkeys = await res.json();
|
|
expect(passkeys).toBeInstanceOf(Array);
|
|
expect(passkeys.length).toBe(0);
|
|
});
|
|
|
|
test('passkey list remains empty without registration', async ({ request }) => {
|
|
const listRes = await request.get(`${WORKER_URL}/user_api/passkey`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
});
|
|
expect(listRes.ok()).toBe(true);
|
|
const passkeys = await listRes.json();
|
|
expect(passkeys).toBeInstanceOf(Array);
|
|
expect(passkeys.length).toBe(0);
|
|
});
|
|
|
|
test('register_response with invalid credential returns 400', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/register_response`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: {
|
|
credential: {
|
|
id: 'fake-id',
|
|
rawId: 'fake-raw-id',
|
|
type: 'public-key',
|
|
response: {
|
|
attestationObject: 'invalid-data',
|
|
clientDataJSON: 'invalid-data',
|
|
},
|
|
},
|
|
origin: 'http://localhost',
|
|
passkey_name: 'test-passkey',
|
|
},
|
|
});
|
|
expect(res.ok()).toBe(false);
|
|
// Should fail verification
|
|
expect(res.status()).toBeGreaterThanOrEqual(400);
|
|
});
|
|
|
|
test('rename nonexistent passkey succeeds silently', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/rename`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: {
|
|
passkey_id: 'nonexistent-id',
|
|
passkey_name: 'new-name',
|
|
},
|
|
});
|
|
// The SQL UPDATE just affects 0 rows, still returns success
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.success).toBe(true);
|
|
});
|
|
|
|
test('rename with invalid name returns 400', async ({ request }) => {
|
|
const res = await request.post(`${WORKER_URL}/user_api/passkey/rename`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
data: {
|
|
passkey_id: 'any-id',
|
|
passkey_name: 'x'.repeat(256),
|
|
},
|
|
});
|
|
expect(res.status()).toBe(400);
|
|
});
|
|
|
|
test('delete nonexistent passkey succeeds silently', async ({ request }) => {
|
|
const res = await request.delete(`${WORKER_URL}/user_api/passkey/nonexistent-id`, {
|
|
headers: { 'x-user-token': userJwt },
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
const body = await res.json();
|
|
expect(body.success).toBe(true);
|
|
});
|
|
});
|