/** * QA-APIKEY-020: API Keys Tests * * E2E Test Suite for API Key Management * - Create API Key * - Revoke API Key * - API Access with Key * - Key Rotation */ import { test, expect } from '@playwright/test'; import { navigateTo, waitForLoading, generateTestScenarioName } from './utils/test-helpers'; import { generateTestUser, loginUserViaUI, registerUserViaAPI, createApiKeyViaAPI, listApiKeys, revokeApiKey, createAuthHeader, createApiKeyHeader, } from './utils/auth-helpers'; // Store test data for cleanup let testUser: { email: string; password: string; fullName: string } | null = null; let accessToken: string | null = null; let apiKey: string | null = null; let apiKeyId: string | null = null; // ============================================ // TEST SUITE: API Key Creation (UI) // ============================================ test.describe('QA-APIKEY-020: Create API Key - UI', () => { test.beforeEach(async ({ page, request }) => { // Register and login user testUser = generateTestUser('APIKey'); const auth = await registerUserViaAPI( request, testUser.email, testUser.password, testUser.fullName ); accessToken = auth.access_token; // Login via UI await loginUserViaUI(page, testUser.email, testUser.password); }); test('should navigate to API Keys settings page', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Verify page loaded await expect(page.getByRole('heading', { name: /api keys|api keys management/i })).toBeVisible(); }); test('should create API key and display modal with full key', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Click create new key button await page.getByRole('button', { name: /create|generate|new.*key/i }).click(); // Fill form await page.getByLabel(/name|key name/i).fill('Test API Key'); // Select scopes if available const scopeCheckboxes = page.locator('input[type="checkbox"][name*="scope"], [data-testid*="scope"]'); if (await scopeCheckboxes.first().isVisible().catch(() => false)) { await scopeCheckboxes.first().check(); } // Submit form await page.getByRole('button', { name: /create|generate|save/i }).click(); // Verify modal appears with the full key const modal = page.locator('[role="dialog"], [data-testid="api-key-modal"], .modal').first(); await expect(modal).toBeVisible({ timeout: 5000 }); // Verify key is displayed await expect(modal.getByText(/mk_/i).or(modal.locator('input[value*="mk_"]'))).toBeVisible(); // Verify warning message await expect( modal.getByText(/copy now|only see once|save.*key|cannot.*see.*again/i).first() ).toBeVisible(); }); test('should copy API key to clipboard', async ({ page, context }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Create a key await page.getByRole('button', { name: /create|generate|new.*key/i }).click(); await page.getByLabel(/name|key name/i).fill('Clipboard Test Key'); await page.getByRole('button', { name: /create|generate|save/i }).click(); // Wait for modal const modal = page.locator('[role="dialog"], [data-testid="api-key-modal"], .modal').first(); await expect(modal).toBeVisible({ timeout: 5000 }); // Click copy button const copyButton = modal.getByRole('button', { name: /copy|clipboard/i }); if (await copyButton.isVisible().catch(() => false)) { await copyButton.click(); // Verify copy success message or toast await expect( page.getByText(/copied|clipboard|success/i).first() ).toBeVisible({ timeout: 3000 }); } }); test('should show API key in list after creation', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Create a key const keyName = 'List Test Key'; await page.getByRole('button', { name: /create|generate|new.*key/i }).click(); await page.getByLabel(/name|key name/i).fill(keyName); await page.getByRole('button', { name: /create|generate|save/i }).click(); // Close modal if present const modal = page.locator('[role="dialog"], [data-testid="api-key-modal"], .modal').first(); if (await modal.isVisible().catch(() => false)) { const closeButton = modal.getByRole('button', { name: /close|done|ok/i }); await closeButton.click(); } // Refresh page await page.reload(); await page.waitForLoadState('networkidle'); // Verify key appears in list await expect(page.getByText(keyName)).toBeVisible(); }); test('should validate required fields when creating API key', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Click create new key button await page.getByRole('button', { name: /create|generate|new.*key/i }).click(); // Submit without filling name await page.getByRole('button', { name: /create|generate|save/i }).click(); // Verify validation error await expect( page.getByText(/required|name.*required|please enter/i).first() ).toBeVisible({ timeout: 5000 }); }); }); // ============================================ // TEST SUITE: API Key Revocation (UI) // ============================================ test.describe('QA-APIKEY-020: Revoke API Key - UI', () => { test.beforeEach(async ({ page, request }) => { // Register and login user testUser = generateTestUser('RevokeKey'); const auth = await registerUserViaAPI( request, testUser.email, testUser.password, testUser.fullName ); accessToken = auth.access_token; // Login via UI await loginUserViaUI(page, testUser.email, testUser.password); }); test('should revoke API key and remove from list', async ({ page, request }) => { // Create an API key via API first const newKey = await createApiKeyViaAPI( request, accessToken!, 'Key To Revoke', ['read:scenarios'] ); // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Find the key in list await expect(page.getByText('Key To Revoke')).toBeVisible(); // Click revoke/delete button const revokeButton = page.locator('tr', { hasText: 'Key To Revoke' }).getByRole('button', { name: /revoke|delete|remove/i }); await revokeButton.click(); // Confirm revocation if confirmation dialog appears const confirmButton = page.getByRole('button', { name: /confirm|yes|revoke/i }); if (await confirmButton.isVisible().catch(() => false)) { await confirmButton.click(); } // Verify key is no longer in list await page.reload(); await page.waitForLoadState('networkidle'); await expect(page.getByText('Key To Revoke')).not.toBeVisible(); }); test('should show confirmation before revoking', async ({ page, request }) => { // Create an API key via API const newKey = await createApiKeyViaAPI( request, accessToken!, 'Key With Confirmation', ['read:scenarios'] ); // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Find and click revoke const revokeButton = page.locator('tr', { hasText: 'Key With Confirmation' }).getByRole('button', { name: /revoke|delete/i }); await revokeButton.click(); // Verify confirmation dialog await expect( page.getByText(/are you sure|confirm.*revoke|cannot.*undo/i).first() ).toBeVisible({ timeout: 5000 }); }); }); // ============================================ // TEST SUITE: API Access with Key (API) // ============================================ test.describe('QA-APIKEY-020: API Access with Key', () => { test.beforeAll(async ({ request }) => { // Register test user testUser = generateTestUser('APIAccess'); const auth = await registerUserViaAPI( request, testUser.email, testUser.password, testUser.fullName ); accessToken = auth.access_token; }); test('should access API with valid API key header', async ({ request }) => { // Create an API key const newKey = await createApiKeyViaAPI( request, accessToken!, 'Valid Access Key', ['read:scenarios'] ); apiKey = newKey.key; apiKeyId = newKey.id; // Make API request with API key const response = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(apiKey), }); // Should be authorized expect(response.status()).not.toBe(401); expect(response.status()).not.toBe(403); }); test('should access /auth/me with valid API key', async ({ request }) => { // Create an API key const newKey = await createApiKeyViaAPI( request, accessToken!, 'Me Endpoint Key', ['read:scenarios'] ); // Make API request const response = await request.get('http://localhost:8000/api/v1/auth/me', { headers: createApiKeyHeader(newKey.key), }); expect(response.ok()).toBeTruthy(); const data = await response.json(); expect(data).toHaveProperty('id'); expect(data).toHaveProperty('email'); }); test('should return 401 with revoked API key', async ({ request }) => { // Create an API key const newKey = await createApiKeyViaAPI( request, accessToken!, 'Key To Revoke For Test', ['read:scenarios'] ); // Revoke the key await revokeApiKey(request, accessToken!, newKey.id); // Try to use revoked key const response = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(newKey.key), }); expect(response.status()).toBe(401); }); test('should return 401 with invalid API key format', async ({ request }) => { const response = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader('invalid_key_format'), }); expect(response.status()).toBe(401); }); test('should return 401 with non-existent API key', async ({ request }) => { const response = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader('mk_nonexistentkey12345678901234'), }); expect(response.status()).toBe(401); }); test('should return 401 without API key header', async ({ request }) => { const response = await request.get('http://localhost:8000/api/v1/scenarios'); // Should require authentication expect(response.status()).toBe(401); }); test('should respect API key scopes', async ({ request }) => { // Create a read-only API key const readKey = await createApiKeyViaAPI( request, accessToken!, 'Read Only Key', ['read:scenarios'] ); // Read should work const readResponse = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(readKey.key), }); // Should be allowed for read operations expect(readResponse.status()).not.toBe(403); }); test('should track API key last used timestamp', async ({ request }) => { // Create an API key const newKey = await createApiKeyViaAPI( request, accessToken!, 'Track Usage Key', ['read:scenarios'] ); // Use the key await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(newKey.key), }); // Check if last_used is updated (API dependent) const listResponse = await request.get('http://localhost:8000/api/v1/api-keys', { headers: createAuthHeader(accessToken!), }); if (listResponse.ok()) { const keys = await listResponse.json(); const key = keys.find((k: { id: string }) => k.id === newKey.id); if (key && key.last_used_at) { expect(key.last_used_at).toBeTruthy(); } } }); }); // ============================================ // TEST SUITE: API Key Management (API) // ============================================ test.describe('QA-APIKEY-020: API Key Management - API', () => { test.beforeAll(async ({ request }) => { // Register test user testUser = generateTestUser('KeyMgmt'); const auth = await registerUserViaAPI( request, testUser.email, testUser.password, testUser.fullName ); accessToken = auth.access_token; }); test('should list all API keys for user', async ({ request }) => { // Create a couple of keys await createApiKeyViaAPI(request, accessToken!, 'Key 1', ['read:scenarios']); await createApiKeyViaAPI(request, accessToken!, 'Key 2', ['read:scenarios', 'write:scenarios']); // List keys const keys = await listApiKeys(request, accessToken!); expect(keys.length).toBeGreaterThanOrEqual(2); expect(keys.some(k => k.name === 'Key 1')).toBe(true); expect(keys.some(k => k.name === 'Key 2')).toBe(true); }); test('should not expose full API key in list response', async ({ request }) => { // Create a key const newKey = await createApiKeyViaAPI(request, accessToken!, 'Hidden Key', ['read:scenarios']); // List keys const keys = await listApiKeys(request, accessToken!); const key = keys.find(k => k.id === newKey.id); expect(key).toBeDefined(); // Should have prefix but not full key expect(key).toHaveProperty('prefix'); expect(key).not.toHaveProperty('key'); expect(key).not.toHaveProperty('key_hash'); }); test('should create API key with expiration', async ({ request }) => { // Create key with 7 day expiration const newKey = await createApiKeyViaAPI( request, accessToken!, 'Expiring Key', ['read:scenarios'], 7 ); expect(newKey).toHaveProperty('id'); expect(newKey).toHaveProperty('key'); expect(newKey.key).toMatch(/^mk_/); }); test('should rotate API key', async ({ request }) => { // Create a key const oldKey = await createApiKeyViaAPI(request, accessToken!, 'Rotatable Key', ['read:scenarios']); // Rotate the key const rotateResponse = await request.post( `http://localhost:8000/api/v1/api-keys/${oldKey.id}/rotate`, { headers: createAuthHeader(accessToken!) } ); if (rotateResponse.status() === 404) { test.skip(true, 'Key rotation endpoint not implemented'); } expect(rotateResponse.ok()).toBeTruthy(); const newKeyData = await rotateResponse.json(); expect(newKeyData).toHaveProperty('key'); expect(newKeyData.key).not.toBe(oldKey.key); // Old key should no longer work const oldKeyResponse = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(oldKey.key), }); expect(oldKeyResponse.status()).toBe(401); // New key should work const newKeyResponse = await request.get('http://localhost:8000/api/v1/scenarios', { headers: createApiKeyHeader(newKeyData.key), }); expect(newKeyResponse.ok()).toBeTruthy(); }); }); // ============================================ // TEST SUITE: API Key UI - List View // ============================================ test.describe('QA-APIKEY-020: API Key List View', () => { test.beforeEach(async ({ page, request }) => { // Register and login user testUser = generateTestUser('ListView'); const auth = await registerUserViaAPI( request, testUser.email, testUser.password, testUser.fullName ); accessToken = auth.access_token; // Login via UI await loginUserViaUI(page, testUser.email, testUser.password); }); test('should display API keys table with correct columns', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Verify table headers await expect(page.getByRole('columnheader', { name: /name/i })).toBeVisible(); await expect(page.getByRole('columnheader', { name: /prefix|key/i })).toBeVisible(); await expect(page.getByRole('columnheader', { name: /scopes|permissions/i })).toBeVisible(); await expect(page.getByRole('columnheader', { name: /created|date/i })).toBeVisible(); await expect(page.getByRole('columnheader', { name: /actions/i })).toBeVisible(); }); test('should show empty state when no API keys', async ({ page }) => { // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Verify empty state message await expect( page.getByText(/no.*keys|no.*api.*keys|get started|create.*key/i).first() ).toBeVisible(); }); test('should display key prefix for identification', async ({ page, request }) => { // Create a key via API const newKey = await createApiKeyViaAPI(request, accessToken!, 'Prefix Test Key', ['read:scenarios']); // Navigate to API Keys page await page.goto('/settings/api-keys'); await page.waitForLoadState('networkidle'); // Verify prefix is displayed await expect(page.getByText(newKey.prefix)).toBeVisible(); }); });