towerops/e2e/tests/api-tokens.spec.ts
Graham McIntire 053ad8b373
feat: add critical e2e tests for robustness and security
- Added error scenarios tests (404s, validation, network errors, edge cases)
- Added navigation and routing tests (browser nav, deep linking, URL state, breadcrumbs)
- Added data integrity tests (unsaved changes, form persistence, concurrent edits)
- Added advanced search/filtering tests (global search, filter combinations, date ranges)
- Added API tokens tests (generation, permissions, revocation, security)

Covers:
- Comprehensive error handling and recovery
- Browser navigation and URL state management
- Data loss prevention and form state
- Advanced search with wildcards and categories
- Complete API token lifecycle and security
- Input validation edge cases (XSS, special chars, length limits)
- Concurrent action prevention
- Filter presets and export functionality
2026-03-06 17:21:14 -06:00

367 lines
12 KiB
TypeScript

import { test, expect } from '@playwright/test';
test.describe('API Tokens and API Access', () => {
test.describe('Personal API Token Management', () => {
test('can access API tokens section', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Look for API tokens section
const apiSection = page.locator(':has-text("API"), :has-text("Token")').first();
if (await apiSection.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(apiSection).toBeVisible();
}
});
test('shows existing API tokens', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Look for token list
const tokenList = page.locator('table, ul, [data-tokens]').first();
if (await tokenList.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tokenList).toBeVisible();
}
});
test('can generate new API token', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate"), button:has-text("Create Token")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(500);
// Token creation form or dialog
const tokenForm = page.locator('[role="dialog"], form').first();
if (await tokenForm.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tokenForm).toBeVisible();
}
}
});
test('new token is displayed only once', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// After generating token, should show warning about copying
const warning = page.locator(':has-text("copy"), :has-text("only shown once"), :has-text("save")').first();
if (await warning.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(warning).toBeVisible();
}
});
test('can copy token to clipboard', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const copyButton = page.locator('button:has-text("Copy"), [data-copy]').first();
if (await copyButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await copyButton.click();
await page.waitForTimeout(300);
// Should show copied confirmation
const confirmation = page.locator(':has-text("Copied"), .success').first();
if (await confirmation.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(confirmation).toBeVisible();
}
}
});
test('token name can be set', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
const nameField = page.locator('input[name*="name"], input[placeholder*="name"]').first();
if (await nameField.isVisible({ timeout: 2000 }).catch(() => false)) {
await nameField.fill('E2E Test Token');
await expect(nameField).toBeVisible();
}
}
});
});
test.describe('Token Permissions', () => {
test('can set token scopes', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for permission checkboxes
const permissionCheckbox = page.locator('input[type="checkbox"], [role="checkbox"]').first();
if (await permissionCheckbox.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(permissionCheckbox).toBeVisible();
}
}
});
test('shows available scopes', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for scope labels
const scopeLabel = page.locator(':has-text("read"), :has-text("write"), :has-text("admin")').first();
if (await scopeLabel.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(scopeLabel).toBeVisible();
}
}
});
});
test.describe('Token Revocation', () => {
test('can revoke API token', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const revokeButton = page.locator('button:has-text("Revoke"), button:has-text("Delete")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(revokeButton).toBeVisible();
}
});
test('shows confirmation before revoking', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const revokeButton = page.locator('button:has-text("Revoke")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await revokeButton.click();
await page.waitForTimeout(300);
const confirmation = page.locator(':has-text("confirm"), :has-text("sure"), [role="dialog"]').first();
if (await confirmation.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(confirmation).toBeVisible();
}
}
});
test('revoked token is removed from list', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const tokenCount = await page.locator('button:has-text("Revoke")').count();
const revokeButton = page.locator('button:has-text("Revoke")').first();
if (await revokeButton.isVisible({ timeout: 2000 }).catch(() => false)) {
// Accept confirmation
page.once('dialog', dialog => dialog.accept());
await revokeButton.click();
await page.waitForTimeout(1000);
// Count should decrease or stay same
const newCount = await page.locator('button:has-text("Revoke")').count();
expect(newCount).toBeLessThanOrEqual(tokenCount);
}
});
});
test.describe('Token Usage Information', () => {
test('shows last used timestamp', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const timestamp = page.locator('time, :has-text("Last used"), :has-text("ago")').first();
if (await timestamp.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(timestamp).toBeVisible();
}
});
test('shows creation date', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const createdDate = page.locator(':has-text("Created"), time').first();
if (await createdDate.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(createdDate).toBeVisible();
}
});
test('shows token expiry if applicable', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const expiry = page.locator(':has-text("Expires"), :has-text("Expiration")').first();
if (await expiry.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(expiry).toBeVisible();
}
});
});
test.describe('API Documentation Access', () => {
test('can access API documentation', async ({ page }) => {
await page.goto('/docs/api');
await expect(page).toHaveURL(/\/docs\/api/);
await expect(page.locator('body')).toBeVisible();
});
test('API docs show authentication instructions', async ({ page }) => {
await page.goto('/docs/api');
const authSection = page.locator(':has-text("Authentication"), :has-text("Bearer"), :has-text("Authorization")').first();
if (await authSection.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(authSection).toBeVisible();
}
});
test('API docs show example requests', async ({ page }) => {
await page.goto('/docs/api');
const codeExample = page.locator('code, pre').first();
if (await codeExample.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(codeExample).toBeVisible();
}
});
test('API docs list available endpoints', async ({ page }) => {
await page.goto('/docs/api');
const endpoint = page.locator(':has-text("/api/"), code:has-text("GET"), code:has-text("POST")').first();
if (await endpoint.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(endpoint).toBeVisible();
}
});
test('can try API in interactive docs', async ({ page }) => {
await page.goto('/docs/api');
const tryButton = page.locator('button:has-text("Try"), button:has-text("Test")').first();
if (await tryButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(tryButton).toBeVisible();
}
});
});
test.describe('Rate Limiting', () => {
test('shows rate limit information', async ({ page }) => {
await page.goto('/docs/api');
const rateLimitInfo = page.locator(':has-text("rate limit"), :has-text("requests per")').first();
if (await rateLimitInfo.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(rateLimitInfo).toBeVisible();
}
});
});
test.describe('Token Security', () => {
test('warns about token security', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
const generateButton = page.locator('button:has-text("Generate")').first();
if (await generateButton.isVisible({ timeout: 2000 }).catch(() => false)) {
await generateButton.click();
await page.waitForTimeout(300);
// Look for security warning
const warning = page.locator(':has-text("secure"), :has-text("secret"), :has-text("keep safe")').first();
if (await warning.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(warning).toBeVisible();
}
}
});
test('token is masked in list', async ({ page }) => {
await page.goto('/users/settings');
if ((await page.url()).includes('sudo-verify')) {
return;
}
// Tokens should be masked (not showing full value)
const maskedToken = page.locator(':has-text("***"), code:has-text("...")').first();
if (await maskedToken.isVisible({ timeout: 2000 }).catch(() => false)) {
await expect(maskedToken).toBeVisible();
}
});
});
});