DOMAIN:TESTING — ADVERSARIAL_TESTING¶
OWNER: ashley
ALSO_USED_BY: marije, judith (vulnerability reports), jasper (gap reports as input)
UPDATED: 2026-03-24
SCOPE: adversarial and chaos testing for all GE client projects
ADVERSARIAL_TESTING_OVERVIEW¶
Ashley is GE's chaos monkey. Ashley tries to BREAK things.
Ashley has ZERO knowledge of the codebase by design.
Ashley tests from the USER perspective only — like an attacker or a confused user.
PHILOSOPHY:
- Developers build for the happy path. Ashley tests for the unhappy path.
- If the system can be broken by a user, it WILL be broken by a user.
- Adversarial testing is NOT about finding bugs in code — it's about finding failures in ASSUMPTIONS.
- The less Ashley knows about the code, the more realistic the testing is.
CONSTRAINT: Ashley NEVER reads source code, never reads test code, never reads specs.
CONSTRAINT: Ashley receives ONLY: the running application URL and a one-sentence description of what it does.
REASON: real attackers and confused users don't have access to source code.
CHAOS_MONKEY_APPROACH¶
THE_MINDSET¶
Think like three personas simultaneously:
1. MALICIOUS_USER: actively trying to exploit the system
2. CONFUSED_USER: clicking things randomly, entering wrong data, navigating unexpectedly
3. IMPATIENT_USER: double-clicking, hitting back, refreshing mid-operation, switching tabs
CHAOS_CATEGORIES¶
CATEGORY_1: INPUT_CHAOS
- What happens with unexpected inputs?
- Goal: find crashes, data corruption, security holes
CATEGORY_2: FLOW_CHAOS
- What happens with unexpected navigation?
- Goal: find broken states, lost data, inconsistent UI
CATEGORY_3: TIMING_CHAOS
- What happens with unexpected timing?
- Goal: find race conditions, stale data, deadlocks
CATEGORY_4: ENVIRONMENT_CHAOS
- What happens with degraded infrastructure?
- Goal: find missing error handling, silent failures
INPUT_FUZZING¶
STRING_INPUTS¶
Test every text input with:
const FUZZ_STRINGS = [
'', // Empty
' ', // Single space
' ', // Multiple spaces
'\t\n\r', // Whitespace characters
'a'.repeat(10000), // Very long string
'<script>alert("xss")</script>', // XSS attempt
"'; DROP TABLE users; --", // SQL injection
'{{constructor.constructor("return this")()}}', // Template injection
'../../../etc/passwd', // Path traversal
'null', // String "null"
'undefined', // String "undefined"
'true', // String "true"
'0', // String "0"
'-1', // Negative number as string
'1e308', // Near max float
'NaN', // String "NaN"
'Infinity', // String "Infinity"
'\u0000', // Null byte
'\uFEFF', // Zero-width no-break space (BOM)
'\u200B', // Zero-width space
'Ω≈ç√∫', // Unicode symbols
'田中太郎', // CJK characters
'مرحبا', // RTL text (Arabic)
'🎉🔥💀', // Emoji
'test\x00hidden', // Null byte in middle
'Robert"); DROP TABLE Students;--', // Bobby Tables
];
NUMBER_INPUTS¶
const FUZZ_NUMBERS = [
0,
-0,
-1,
1,
Number.MAX_SAFE_INTEGER,
Number.MIN_SAFE_INTEGER,
Number.MAX_VALUE,
Number.MIN_VALUE,
Number.POSITIVE_INFINITY,
Number.NEGATIVE_INFINITY,
NaN,
1.7976931348623157e+308, // Near overflow
5e-324, // Smallest positive
0.1 + 0.2, // Floating point imprecision
999999999999999999999, // Beyond safe integer
-999999999999999999999,
3.14159265358979323846, // Many decimals
1e100, // Very large
1e-100, // Very small
];
FILE_UPLOAD_FUZZING¶
const FUZZ_FILES = [
{ name: 'empty.txt', content: '', type: 'text/plain' },
{ name: 'huge.txt', content: 'A'.repeat(100_000_000), type: 'text/plain' },
{ name: 'fake.jpg', content: 'not an image', type: 'image/jpeg' },
{ name: '../../../etc/passwd', content: 'path traversal', type: 'text/plain' },
{ name: 'test.svg', content: '<svg onload="alert(1)">', type: 'image/svg+xml' },
{ name: 'file.exe', content: 'MZ...', type: 'application/octet-stream' },
{ name: 'test.html', content: '<script>alert(1)</script>', type: 'text/html' },
{ name: '', content: 'no name', type: 'text/plain' },
{ name: 'a'.repeat(500) + '.txt', content: 'long name', type: 'text/plain' },
{ name: 'file name with spaces.txt', content: 'spaces', type: 'text/plain' },
{ name: 'file%20encoded.txt', content: 'encoded', type: 'text/plain' },
];
RULE: every input field gets fuzzed — no exceptions
RULE: file uploads get content-type mismatches, oversized files, malicious names
RULE: document EVERY crash, error, or unexpected behavior — even "minor" ones
RACE_CONDITION_TESTING¶
DOUBLE_SUBMIT¶
test('double-click submit does not create duplicate', async ({ page }) => {
await page.goto('/items/new');
await page.getByLabel('Name').fill('Test Item');
const submitButton = page.getByRole('button', { name: 'Create' });
// Rapid double click
await submitButton.dblclick();
// Wait for any responses
await page.waitForTimeout(2000);
// Navigate to list and verify only one item created
await page.goto('/items');
const items = page.getByText('Test Item');
await expect(items).toHaveCount(1);
});
PARALLEL_OPERATIONS¶
test('concurrent edits to same resource', async ({ browser }) => {
// Two browser contexts = two users
const context1 = await browser.newContext();
const context2 = await browser.newContext();
const page1 = await context1.newPage();
const page2 = await context2.newPage();
// Both navigate to same item
await page1.goto('/items/123/edit');
await page2.goto('/items/123/edit');
// Both change the name
await page1.getByLabel('Name').fill('Name from User 1');
await page2.getByLabel('Name').fill('Name from User 2');
// Both submit simultaneously
await Promise.all([
page1.getByRole('button', { name: 'Save' }).click(),
page2.getByRole('button', { name: 'Save' }).click(),
]);
// Verify: one should succeed, other should get conflict error
// The system must NOT silently lose an update
await page1.reload();
const name = await page1.getByLabel('Name').inputValue();
// name should be one of the two — not a corrupted mix
expect(['Name from User 1', 'Name from User 2']).toContain(name);
await context1.close();
await context2.close();
});
BACK_BUTTON_CHAOS¶
test('back button after form submit does not re-submit', async ({ page }) => {
await page.goto('/items/new');
await page.getByLabel('Name').fill('Test');
await page.getByRole('button', { name: 'Create' }).click();
// Wait for redirect to success page
await page.waitForURL('/items/**');
// Hit back button
await page.goBack();
// Should NOT re-submit the form
// Check that going forward doesn't create a duplicate
await page.goForward();
await page.goto('/items');
const items = page.getByText('Test');
await expect(items).toHaveCount(1);
});
ERROR_INJECTION¶
NETWORK_ERRORS¶
test('handles network failure gracefully', async ({ page }) => {
await page.goto('/dashboard');
// Kill all API calls
await page.route('**/api/**', (route) => route.abort('connectionrefused'));
// Trigger a data refresh
await page.getByRole('button', { name: 'Refresh' }).click();
// System should show error, not crash
await expect(page.getByText(/error|failed|unavailable/i)).toBeVisible();
// Should NOT show raw error stack, JSON, or "undefined"
await expect(page.locator('text=/undefined|null|NaN|\\{|\\[/')).not.toBeVisible();
});
SLOW_NETWORK¶
test('handles slow API responses', async ({ page }) => {
// Simulate 10-second delay on all API calls
await page.route('**/api/**', async (route) => {
await new Promise((r) => setTimeout(r, 10000));
await route.continue();
});
await page.goto('/dashboard');
// Should show loading state, not blank page
await expect(page.getByText(/loading|please wait/i)).toBeVisible();
// Should NOT show error prematurely
await expect(page.getByText(/error|failed/i)).not.toBeVisible();
});
MALFORMED_RESPONSES¶
test('handles malformed API response', async ({ page }) => {
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: '{"users": [{"name": null, "email": 123, "extra_field": "unexpected"}]}',
});
});
await page.goto('/users');
// Should NOT crash — should handle gracefully
// Should NOT show raw JSON or "undefined"
await expect(page.locator('body')).not.toContainText('undefined');
await expect(page.locator('body')).not.toContainText('[object Object]');
});
test('handles non-JSON response on JSON endpoint', async ({ page }) => {
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'text/html',
body: '<html><body>Not JSON</body></html>',
});
});
await page.goto('/users');
// Should show error state, not crash
await expect(page.getByText(/error|something went wrong/i)).toBeVisible();
});
BOUNDARY_VALUE_ATTACKS¶
PAGINATION_BOUNDARIES¶
test('handles page=0 in URL', async ({ page }) => {
await page.goto('/items?page=0');
// Should redirect to page 1 or show first page, not crash
await expect(page.locator('body')).not.toContainText('Error');
});
test('handles page=-1 in URL', async ({ page }) => {
await page.goto('/items?page=-1');
await expect(page.locator('body')).not.toContainText('Error');
});
test('handles page=999999 in URL', async ({ page }) => {
await page.goto('/items?page=999999');
// Should show empty state or redirect, not crash
await expect(page.getByText(/no items|no results|empty/i)).toBeVisible();
});
test('handles page=abc in URL', async ({ page }) => {
await page.goto('/items?page=abc');
// Should handle gracefully — not crash, not show all items
await expect(page.locator('body')).not.toContainText('NaN');
});
URL_MANIPULATION¶
test('handles nonexistent route', async ({ page }) => {
const response = await page.goto('/this-route-does-not-exist-12345');
// Should show 404 page, not error stack
expect(response?.status()).toBe(404);
await expect(page.getByText(/not found|404/i)).toBeVisible();
});
test('handles accessing other user resources', async ({ page }) => {
// Logged in as user A, trying to access user B's data
await page.goto('/users/other-user-id/settings');
// Should get 403 or redirect, not show other user's data
const status = await page.evaluate(() => document.title);
expect(status).not.toContain('Settings'); // Should not show settings page
});
UNEXPECTED_USER_FLOWS¶
SESSION_EXPIRY¶
test('handles expired session mid-operation', async ({ page, context }) => {
// Start a form
await page.goto('/items/new');
await page.getByLabel('Name').fill('Important Item');
// Clear all cookies to simulate session expiry
await context.clearCookies();
// Try to submit
await page.getByRole('button', { name: 'Create' }).click();
// Should redirect to login, not lose data silently
await expect(page).toHaveURL(/login/);
});
TAB_SWITCHING¶
test('handles long idle with stale data', async ({ page }) => {
await page.goto('/dashboard');
const initialData = await page.getByTestId('data-timestamp').textContent();
// Simulate tab becoming visible after long idle
await page.evaluate(() => {
document.dispatchEvent(new Event('visibilitychange'));
});
// Data should refresh, not show stale
// (This tests if the app handles visibility change)
});
RAPID_NAVIGATION¶
test('rapid navigation does not crash', async ({ page }) => {
const routes = ['/dashboard', '/items', '/settings', '/profile', '/items/new'];
// Navigate rapidly without waiting for load
for (const route of routes) {
page.goto(route); // Intentionally NOT awaiting
}
// Wait for last navigation to settle
await page.waitForLoadState('networkidle');
// Should be on one of the routes, not crashed
const url = page.url();
expect(routes.some((r) => url.includes(r))).toBe(true);
});
ACCESSIBILITY_EDGE_CASES¶
KEYBOARD_ONLY_NAVIGATION¶
test('entire app is keyboard navigable', async ({ page }) => {
await page.goto('/');
// Tab through the entire page
for (let i = 0; i < 50; i++) {
await page.keyboard.press('Tab');
const focused = await page.evaluate(() => {
const el = document.activeElement;
return {
tag: el?.tagName,
role: el?.getAttribute('role'),
visible: el?.getBoundingClientRect().height > 0,
};
});
// Focus should ALWAYS be visible — no focus traps in hidden elements
if (focused.tag !== 'BODY') {
expect(focused.visible).toBe(true);
}
}
});
SCREEN_READER_ANNOUNCEMENTS¶
test('form errors are announced', async ({ page }) => {
await page.goto('/form');
// Submit empty form
await page.getByRole('button', { name: 'Submit' }).click();
// Errors should be in an aria-live region or use role="alert"
const alerts = page.getByRole('alert');
await expect(alerts.first()).toBeVisible();
});
ZOOM_AND_RESIZE¶
test('app works at 200% zoom', async ({ page }) => {
await page.goto('/');
// Simulate 200% zoom via viewport scaling
await page.setViewportSize({ width: 640, height: 360 });
// Core navigation should still be accessible
await expect(page.getByRole('navigation')).toBeVisible();
// No horizontal scrollbar on content
const hasHScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHScroll).toBe(false);
});
ASHLEY_WORKFLOW¶
STEP 1: Receive application URL and one-line description
STEP 2: Explore the application like a first-time user (NO documentation, NO codebase)
STEP 3: Map all user-facing features and entry points
STEP 4: Run fuzz tests against every input
STEP 5: Test race conditions on all state-changing operations
STEP 6: Inject errors on all network calls
STEP 7: Test boundary values on all URL parameters
STEP 8: Test unexpected flows (back button, session expiry, rapid nav)
STEP 9: Test accessibility edge cases (keyboard, zoom, screen reader)
STEP 10: Document ALL findings with reproduction steps
FINDING_REPORT_FORMAT¶
## Finding: [Short title]
Severity: CRITICAL | HIGH | MEDIUM | LOW
Category: input | flow | timing | environment | accessibility
Steps to reproduce:
1. [step]
2. [step]
3. [step]
Expected: [what should happen]
Actual: [what actually happens]
Evidence: [screenshot path or error text]
RULE: every finding has EXACT reproduction steps — "it sometimes crashes" is not a finding
RULE: severity based on user impact, not technical complexity
RULE: CRITICAL = data loss, security breach, total crash
RULE: HIGH = feature broken, wrong data shown, accessibility barrier
RULE: MEDIUM = confusing UX, unhandled error state, cosmetic data issue
RULE: LOW = minor cosmetic, edge case unlikely in production
CROSS_REFERENCES¶
PLAYWRIGHT: domains/testing/playwright-e2e.md — Playwright mechanics used in adversarial tests
RECONCILIATION: domains/testing/test-reconciliation.md — Jasper's gaps inform Ashley's targets
PITFALLS: domains/testing/pitfalls.md — adversarial anti-patterns
ACCESSIBILITY: domains/accessibility/index.md — full accessibility standards