Skip to content

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