mirror of
https://github.com/mgeeky/decode-spam-headers.git
synced 2026-02-22 13:33:30 +01:00
MAESTRO: add rate-limit e2e captcha flow
This commit is contained in:
@@ -74,7 +74,7 @@ All tasks in this phase are parallelizable [P] since they are independent E2E sp
|
|||||||
- [x] T057 [P] Create `frontend/e2e/test-selection.spec.ts` — test US2: open test selector → verify 106+ tests listed → click Deselect All → select 3 specific tests → analyse → verify only 3 results in report. Test search/filter narrows visible tests. Test DNS and decode-all toggle states persist through analysis
|
- [x] T057 [P] Create `frontend/e2e/test-selection.spec.ts` — test US2: open test selector → verify 106+ tests listed → click Deselect All → select 3 specific tests → analyse → verify only 3 results in report. Test search/filter narrows visible tests. Test DNS and decode-all toggle states persist through analysis
|
||||||
- [x] T058 [P] Create `frontend/e2e/report-interaction.spec.ts` — test US4 report features: expand all cards → collapse all → search for a term → verify filtered results → clear search → export JSON → verify downloaded file is valid JSON. Export HTML → verify downloaded file contains styled content
|
- [x] T058 [P] Create `frontend/e2e/report-interaction.spec.ts` — test US4 report features: expand all cards → collapse all → search for a term → verify filtered results → clear search → export JSON → verify downloaded file is valid JSON. Export HTML → verify downloaded file contains styled content
|
||||||
- [x] T059 [P] Create `frontend/e2e/browser-cache.spec.ts` — test US5: complete analysis → reload page → verify headers and results restored from cache → click Clear Cache → verify input and report cleared → reload → verify empty state
|
- [x] T059 [P] Create `frontend/e2e/browser-cache.spec.ts` — test US5: complete analysis → reload page → verify headers and results restored from cache → click Clear Cache → verify input and report cleared → reload → verify empty state
|
||||||
- [ ] T060 [P] Create `frontend/e2e/rate-limiting.spec.ts` — test US6 rate limiting flow: submit requests until 429 response → verify CAPTCHA modal appears → solve CAPTCHA → verify bypass token stored → retry original request succeeds. Test that the CAPTCHA modal is keyboard accessible and visually correct
|
- [x] T060 [P] Create `frontend/e2e/rate-limiting.spec.ts` — test US6 rate limiting flow: submit requests until 429 response → verify CAPTCHA modal appears → solve CAPTCHA → verify bypass token stored → retry original request succeeds. Test that the CAPTCHA modal is keyboard accessible and visually correct
|
||||||
- [ ] T061 [P] Create `frontend/e2e/visual-regression.spec.ts` — screenshot-based visual testing at 4 viewports (320×568, 768×1024, 1280×720, 2560×1080). Capture: landing page (empty state), landing page with headers pasted, progress indicator active, report view with results expanded, hop chain visualisation. Use `expect(page).toHaveScreenshot()` with `animations: 'disabled'` and `mask` for dynamic content (timestamps, elapsed time). Baselines stored in `frontend/e2e/__snapshots__/`
|
- [ ] T061 [P] Create `frontend/e2e/visual-regression.spec.ts` — screenshot-based visual testing at 4 viewports (320×568, 768×1024, 1280×720, 2560×1080). Capture: landing page (empty state), landing page with headers pasted, progress indicator active, report view with results expanded, hop chain visualisation. Use `expect(page).toHaveScreenshot()` with `animations: 'disabled'` and `mask` for dynamic content (timestamps, elapsed time). Baselines stored in `frontend/e2e/__snapshots__/`
|
||||||
- [ ] T062 [P] Create `frontend/e2e/accessibility.spec.ts` — WCAG 2.1 AA audit using `@axe-core/playwright`. Run `AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze()` on: landing page (empty), landing page with input, report view, CAPTCHA modal (if rate-limited). Assert zero violations. Document any necessary exceptions with justification
|
- [ ] T062 [P] Create `frontend/e2e/accessibility.spec.ts` — WCAG 2.1 AA audit using `@axe-core/playwright`. Run `AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze()` on: landing page (empty), landing page with input, report view, CAPTCHA modal (if rate-limited). Assert zero violations. Document any necessary exceptions with justification
|
||||||
- [ ] T063 [P] Create `frontend/e2e/responsive.spec.ts` — viewport matrix test at breakpoints 320px, 768px, 1024px, 1440px, 2560px. At each viewport: verify no horizontal scrollbar, all interactive elements visible and clickable, text readable (no overflow/clipping), report cards stack correctly on narrow viewports. Use `page.setViewportSize()` for per-test overrides
|
- [ ] T063 [P] Create `frontend/e2e/responsive.spec.ts` — viewport matrix test at breakpoints 320px, 768px, 1024px, 1440px, 2560px. At each viewport: verify no horizontal scrollbar, all interactive elements visible and clickable, text readable (no overflow/clipping), report cards stack correctly on narrow viewports. Use `page.setViewportSize()` for per-test overrides
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { test, expect } from "@playwright/test";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const headersPath = path.resolve(__dirname, "../../backend/tests/fixtures/sample_headers.txt");
|
import { AnalyzerPage } from "./pages/analyzer-page";
|
||||||
|
|
||||||
|
const headersPath = path.resolve(__dirname, "fixtures/sample-headers.txt");
|
||||||
const rateLimit = 3;
|
const rateLimit = 3;
|
||||||
const bypassToken = "playwright-bypass-token";
|
const bypassToken = "playwright-bypass-token";
|
||||||
const captchaChallenge = {
|
const captchaChallenge = {
|
||||||
@@ -10,20 +12,47 @@ const captchaChallenge = {
|
|||||||
imageBase64:
|
imageBase64:
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFgwJ/l0pNqQAAAABJRU5ErkJggg==",
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFgwJ/l0pNqQAAAABJRU5ErkJggg==",
|
||||||
};
|
};
|
||||||
|
const successReport = {
|
||||||
|
results: [],
|
||||||
|
hopChain: [],
|
||||||
|
securityAppliances: [],
|
||||||
|
metadata: {
|
||||||
|
totalTests: 0,
|
||||||
|
passedTests: 0,
|
||||||
|
failedTests: 0,
|
||||||
|
skippedTests: 0,
|
||||||
|
elapsedMs: 1,
|
||||||
|
timedOut: false,
|
||||||
|
incompleteTests: [] as string[],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const corsHeaders = {
|
||||||
|
"access-control-allow-origin": "http://localhost:3100",
|
||||||
|
"access-control-allow-credentials": "true",
|
||||||
|
};
|
||||||
|
|
||||||
|
const successSseBody = `event: result\ndata: ${JSON.stringify(successReport)}\n\n`;
|
||||||
|
|
||||||
test("rate limiting shows captcha and retries successfully", async ({ page }) => {
|
test("rate limiting shows captcha and retries successfully", async ({ page }) => {
|
||||||
const headers = await fs.readFile(headersPath, "utf8");
|
const headers = await fs.readFile(headersPath, "utf8");
|
||||||
|
const analyzer = new AnalyzerPage(page);
|
||||||
let analyseCount = 0;
|
let analyseCount = 0;
|
||||||
|
const bypassTokens: string[] = [];
|
||||||
|
|
||||||
await page.route("**/api/analyse", async (route) => {
|
await page.route("**/api/analyse", async (route) => {
|
||||||
analyseCount += 1;
|
analyseCount += 1;
|
||||||
|
const bypassHeader = route.request().headers()["x-captcha-bypass-token"];
|
||||||
|
if (bypassHeader) {
|
||||||
|
bypassTokens.push(bypassHeader);
|
||||||
|
}
|
||||||
|
|
||||||
if (analyseCount === rateLimit + 1) {
|
if (analyseCount === rateLimit + 1) {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 429,
|
status: 429,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
headers: {
|
headers: {
|
||||||
"access-control-allow-origin": "http://localhost:3100",
|
...corsHeaders,
|
||||||
"access-control-allow-credentials": "true",
|
|
||||||
"retry-after": "60",
|
"retry-after": "60",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -34,25 +63,26 @@ test("rate limiting shows captcha and retries successfully", async ({ page }) =>
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await route.continue();
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/event-stream",
|
||||||
|
headers: corsHeaders,
|
||||||
|
body: successSseBody,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.route("**/api/captcha/verify", async (route) => {
|
await page.route("**/api/captcha/verify", async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: "application/json",
|
contentType: "application/json",
|
||||||
headers: {
|
headers: corsHeaders,
|
||||||
"access-control-allow-origin": "http://localhost:3100",
|
|
||||||
"access-control-allow-credentials": "true",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ success: true, bypassToken }),
|
body: JSON.stringify({ success: true, bypassToken }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.goto("http://localhost:3100");
|
await analyzer.goto();
|
||||||
|
await analyzer.pasteHeaders(headers);
|
||||||
const headerInput = page.getByRole("textbox", { name: "Header Input" });
|
|
||||||
await headerInput.fill(headers);
|
|
||||||
|
|
||||||
const analyseButton = page.getByRole("button", { name: "Analyse Headers" });
|
const analyseButton = page.getByRole("button", { name: "Analyse Headers" });
|
||||||
|
|
||||||
@@ -79,9 +109,34 @@ test("rate limiting shows captcha and retries successfully", async ({ page }) =>
|
|||||||
|
|
||||||
expect(rateLimitResponse.status()).toBe(429);
|
expect(rateLimitResponse.status()).toBe(429);
|
||||||
|
|
||||||
const captchaModal = page.getByTestId("captcha-challenge");
|
const captchaModal = analyzer.getCaptchaModal();
|
||||||
await expect(captchaModal).toBeVisible();
|
await expect(captchaModal).toBeVisible();
|
||||||
|
|
||||||
|
const captchaTitle = page.getByRole("heading", { name: "Security Check Required" });
|
||||||
|
const captchaImage = page.getByTestId("captcha-image");
|
||||||
|
const captchaInput = page.getByTestId("captcha-input");
|
||||||
|
const captchaClose = page.getByTestId("captcha-close");
|
||||||
|
const captchaSubmit = page.getByTestId("captcha-submit");
|
||||||
|
|
||||||
|
await expect(captchaTitle).toBeVisible();
|
||||||
|
await expect(captchaImage).toBeVisible();
|
||||||
|
await expect(captchaImage).toHaveAttribute("src", /^data:image\/png;base64,/);
|
||||||
|
await expect(captchaClose).toBeVisible();
|
||||||
|
await expect(captchaSubmit).toBeVisible();
|
||||||
|
await expect(captchaInput).toBeFocused();
|
||||||
|
|
||||||
|
await page.keyboard.press("Shift+Tab");
|
||||||
|
await expect(captchaClose).toBeFocused();
|
||||||
|
|
||||||
|
await page.keyboard.press("Shift+Tab");
|
||||||
|
await expect(captchaSubmit).toBeFocused();
|
||||||
|
|
||||||
|
await page.keyboard.press("Tab");
|
||||||
|
await expect(captchaClose).toBeFocused();
|
||||||
|
|
||||||
|
await captchaInput.focus();
|
||||||
|
await captchaInput.fill("12345");
|
||||||
|
|
||||||
const retryResponsePromise = page.waitForResponse((response) => {
|
const retryResponsePromise = page.waitForResponse((response) => {
|
||||||
if (!response.url().includes("/api/analyse")) {
|
if (!response.url().includes("/api/analyse")) {
|
||||||
return false;
|
return false;
|
||||||
@@ -93,12 +148,29 @@ test("rate limiting shows captcha and retries successfully", async ({ page }) =>
|
|||||||
return bypassHeader === bypassToken;
|
return bypassHeader === bypassToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId("captcha-input").fill("12345");
|
await page.keyboard.press("Enter");
|
||||||
await page.getByTestId("captcha-submit").click();
|
|
||||||
|
|
||||||
const retryResponse = await retryResponsePromise;
|
const retryResponse = await retryResponsePromise;
|
||||||
expect(retryResponse.status()).toBe(200);
|
expect(retryResponse.status()).toBe(200);
|
||||||
|
|
||||||
await expect(captchaModal).toBeHidden({ timeout: 30000 });
|
await expect(captchaModal).toBeHidden({ timeout: 30000 });
|
||||||
await expect(analyseButton).toBeEnabled({ timeout: 30000 });
|
await expect(analyseButton).toBeEnabled({ timeout: 30000 });
|
||||||
|
|
||||||
|
const followUpResponsePromise = page.waitForResponse((response) => {
|
||||||
|
if (!response.url().includes("/api/analyse")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.status() !== 200) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const bypassHeader = response.request().headers()["x-captcha-bypass-token"];
|
||||||
|
return bypassHeader === bypassToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
await analyseButton.click();
|
||||||
|
await followUpResponsePromise;
|
||||||
|
|
||||||
|
const bypassHits = bypassTokens.filter((token) => token === bypassToken);
|
||||||
|
expect(bypassHits.length).toBeGreaterThanOrEqual(2);
|
||||||
|
await expect(analyseButton).toBeEnabled({ timeout: 30000 });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user