# RealWorld (Conduit) — Administrator & Developer Guide

## 1. Architecture Overview

RealWorld is not a deployable application — it is a **specification repository** that defines the "Conduit" blogging platform contract. The repository provides three core deliverables: an OpenAPI specification defining the REST API contract, a multi-runner test infrastructure for validating implementations, and an Astro-based documentation site publishing the specification for implementors.

The architecture follows a spec-first design: the OpenAPI document (`specs/api/openapi.yml`) is the single source of truth. Test suites in two formats (Hurl and Bruno) validate implementations against this contract. A Playwright E2E suite validates frontend implementations. All three layers reference the same behavioral expectations but target different audiences (backend devs, API explorers, and frontend devs respectively).

```text
specs/api/openapi.yml          ← API contract (source of truth)
     │
     ├── specs/api/hurl/*.hurl ← Backend compliance tests (canonical)
     │       │
     │       └── specs/api/hurl-to-bruno.js  → specs/api/bruno/  (generated)
     │
     ├── specs/e2e/*.spec.ts   ← Frontend E2E tests (Playwright)
     │       │
     │       └── specs/e2e/helpers/  ← Shared test utilities
     │
     ├── docs/                 ← Specification documentation site (Astro/Starlight)
     │
     └── assets/               ← Shared media (logos, icons, theme CSS)
```

### Component Inventory

| Component | Path | Role |
|---|---|---|
| OpenAPI Spec | `specs/api/openapi.yml` | REST API contract definition (OpenAPI 3.1, 15 endpoints) |
| Hurl Tests | `specs/api/hurl/*.hurl` | Backend compliance validation (14 test files) |
| Hurl Runner | `specs/api/run-api-tests-hurl.sh` | Shell wrapper for sequential Hurl execution |
| Hurl-to-Bruno Converter | `specs/api/hurl-to-bruno.js` | Bun script converting Hurl to Bruno format |
| Bruno Collection | `specs/api/bruno/` | Generated API test collection for Bruno app/CLI |
| Bruno Runner | `specs/api/run-api-tests-bruno.sh` | Shell wrapper for sequential Bruno execution |
| Playwright E2E Tests | `specs/e2e/*.spec.ts` | Frontend behavioral tests (13 spec files, 80+ tests) |
| Playwright Base Config | `specs/e2e/playwright.base.ts` | Shared test configuration for implementations |
| E2E Test Helpers | `specs/e2e/helpers/` | 8 helper modules (api, articles, auth, comments, config, debug, profile, setup) |
| Documentation Site | `docs/` | Astro + Starlight + React + Tailwind CSS site |
| Theme Stylesheet | `assets/theme/styles.css` | Shared Conduit CSS theme |
| Logo Generator | `assets/media/conduit-logo.svg.generate.ts` | Bun script generating SVG logo from Caudex Bold font |
| Mobile Icons | `assets/media/mobile_icons/` | iOS, Android, watchOS, iMessage app icons |

### Technology Stack

| Layer | Technology | Version |
|---|---|---|
| API Specification | OpenAPI | 3.1.0 |
| Backend Testing | Hurl | 4.0+ |
| API Testing (Alt) | Bruno | 1.0+ |
| E2E Testing | Playwright | Latest |
| E2E Test Language | TypeScript | 5.9+ |
| Documentation | Astro + Starlight | 5.16+ / 0.37+ |
| UI Framework (Docs) | React | 19.2+ |
| CSS (Docs) | Tailwind CSS | 4.1+ |
| Build/Script Runtime | Bun | 1.0+ |
| Image Processing (Docs) | sharp | 0.34+ |

> [!NOTE]
> The repository has no application runtime dependencies. All dependencies belong to either the documentation site (`docs/package.json`) or the tooling scripts. Implementations bring their own stack.

### Data Flow: API Test Execution

```text
1. User sets HOST=http://localhost:PORT
2. run-api-tests-hurl.sh generates unique uid (timestamp + PID)
3. Script discovers all *.hurl files, runs sequentially (--jobs 1)
4. Each .hurl file:
   a. Registers test user(s) with uid-based unique credentials
   b. Performs CRUD operations against the API
   c. Captures variables (tokens, slugs, comment IDs) via [Captures]
   d. Asserts response status codes and JSON body via [Asserts]
5. Exit code 0 = all passed, non-zero = failures
```

### Data Flow: Hurl-to-Bruno Conversion

```text
1. hurl-to-bruno.js reads all *.hurl files from specs/api/hurl/
2. parseHurlFile() extracts: method, URL, headers, JSON body,
   status code, captures, and jsonpath assertions
3. Each Hurl file becomes a Bruno folder (underscores → hyphens)
4. Each request within a file becomes a numbered .bru file
5. Hurl captures → bru.setVar() in script:post-response
6. Hurl jsonpath assertions → Chai expect() assertions
7. Collection-level pre-request generates unique uid variable
8. --check mode: generates to temp dir, diffs against existing
```

## 2. System Requirements

### For Running API Tests

| Requirement | Minimum | Notes |
|---|---|---|
| Bash | 4.0+ | Test runner scripts use `bash` with `set -euo pipefail` |
| Hurl | 4.0+ | Backend compliance testing |
| Bun | 1.0+ | Hurl-to-Bruno converter (`hurl-to-bruno.js`) |
| Bruno CLI | 1.0+ | Alternative test runner (optional) |

### For Running E2E Tests

| Requirement | Minimum | Notes |
|---|---|---|
| Node.js | 18+ | Playwright runtime |
| Playwright | Latest | `npx playwright install --with-deps chromium` |
| TypeScript | 5.9+ | Test files are `.ts` |

### For Building the Documentation Site

| Requirement | Minimum | Notes |
|---|---|---|
| Node.js | 18+ | Astro runtime |
| npm | 9+ | Package management |

### Disk Space

The repository is 309 files / ~104K characters of source. The `docs/` `node_modules` adds ~200MB. Mobile icon assets contribute the bulk of the file count (70+ PNG files across density buckets).

## 3. Configuration Reference

### Environment Variables

| Variable | Type | Default | Description | Required? |
|---|---|---|---|---|
| `HOST` | URL | `http://localhost:8000` (Hurl) / `http://localhost:3000` (Bruno) | Target API server for shell-based test runners | No |
| `UID_VAL` | String | `$(date +%s)$$` | Unique test run identifier for user/email generation in Hurl tests | No |
| `API_BASE` | URL | `https://api.realworld.show/api` | Target API server for Playwright E2E tests | No |
| `API_MODE` | String | `true` | Set to `false` to disable API-dependent E2E tests and use form-based flows | No |
| `CI` | Boolean | — | When set, Playwright uses `forbidOnly: true` and `retries: 2` | No |

> [!IMPORTANT]
> `API_BASE` defaults to the public demo server at `https://api.realworld.show/api`. For local development, always override this to avoid running tests against production.

### Configuration Files

| File | Format | Location | Purpose |
|---|---|---|---|
| `specs/api/openapi.yml` | OpenAPI 3.1 YAML | `specs/api/` | API contract definition |
| `specs/api/bruno/bruno.json` | JSON | `specs/api/bruno/` | Bruno collection metadata |
| `specs/api/bruno/collection.bru` | BRU | `specs/api/bruno/` | Collection-level pre-request script (uid generation) |
| `specs/api/bruno/environments/local.bru` | BRU | `specs/api/bruno/environments/` | Local environment variables (`host`) |
| `specs/e2e/playwright.base.ts` | TypeScript | `specs/e2e/` | Base Playwright configuration |
| `specs/e2e/helpers/config.ts` | TypeScript | `specs/e2e/helpers/` | E2E environment variable resolution |
| `docs/package.json` | JSON | `docs/` | Docs site dependencies and scripts |
| `docs/astro.config.mjs` | ESM JavaScript | `docs/` | Astro/Starlight configuration |
| `docs/tsconfig.json` | JSON | `docs/` | TypeScript configuration for docs site |

### Playwright Base Configuration (Exact Values)

```typescript
{
  testDir: './e2e',
  fullyParallel: false,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 1,
  workers: 1,
  reporter: 'html',
  timeout: 15_000,
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    actionTimeout: 5_000,
    navigationTimeout: 10_000,
  },
  expect: { timeout: 5_000 },
  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
}
```

> [!WARNING]
> `workers: 1` and `fullyParallel: false` are intentional constraints. Tests share backend state (created users, articles) and parallel execution causes flaky failures from resource exhaustion.

### OpenAPI Specification: Endpoint Summary

| Method | Path | Auth | Operation |
|---|---|---|---|
| `POST` | `/users` | None | Register new user |
| `POST` | `/users/login` | None | Login existing user |
| `GET` | `/user` | Token | Get current user |
| `PUT` | `/user` | Token | Update current user |
| `GET` | `/profiles/{username}` | Optional | Get user profile |
| `POST` | `/profiles/{username}/follow` | Token | Follow user |
| `DELETE` | `/profiles/{username}/follow` | Token | Unfollow user |
| `GET` | `/articles` | Optional | List articles (filterable by tag, author, favorited) |
| `POST` | `/articles` | Token | Create article |
| `GET` | `/articles/feed` | Token | Get feed (articles from followed users) |
| `GET` | `/articles/{slug}` | None | Get single article |
| `PUT` | `/articles/{slug}` | Token | Update article |
| `DELETE` | `/articles/{slug}` | Token | Delete article |
| `GET` | `/articles/{slug}/comments` | Optional | List comments |
| `POST` | `/articles/{slug}/comments` | Token | Create comment |
| `DELETE` | `/articles/{slug}/comments/{id}` | Token | Delete comment |
| `POST` | `/articles/{slug}/favorite` | Token | Favorite article |
| `DELETE` | `/articles/{slug}/favorite` | Token | Unfavorite article |
| `GET` | `/tags` | None | List all tags |

### OpenAPI Schema: Data Models

| Schema | Required Fields | Nullable Fields |
|---|---|---|
| `User` | `email`, `token`, `username`, `bio`, `image` | `bio`, `image` |
| `NewUser` | `username`, `email`, `password` | — |
| `UpdateUser` | — (all optional) | `bio`, `image` |
| `Profile` | `username`, `bio`, `image`, `following` | `bio`, `image` |
| `Article` | `slug`, `title`, `description`, `body`, `tagList`, `createdAt`, `updatedAt`, `favorited`, `favoritesCount`, `author` | — |
| `NewArticle` | `title`, `description`, `body` | — |
| `UpdateArticle` | — (all optional) | — |
| `Comment` | `id`, `createdAt`, `updatedAt`, `body`, `author` | — |
| `NewComment` | `body` | — |

### Authentication Scheme

The API uses a custom `Token` scheme (not `Bearer`):

```text
Authorization: Token eyJhbGciOiJIUzI1NiIs...
```

Defined as `apiKey` type in the `securitySchemes` section, applied via the `Authorization` header. Tokens are JWTs returned by the registration and login endpoints.

### Error Response Format

All errors use the `GenericErrorModel` envelope:

```json
{
  "errors": {
    "field_name": ["error message 1", "error message 2"]
  }
}
```

Standard HTTP status codes:
- `401` Unauthorized — missing or invalid token
- `403` Forbidden — user lacks permission for the operation
- `404` Not Found — resource doesn't exist
- `409` Conflict — duplicate resource (username/email)
- `422` Unprocessable Entity — validation error

### Pagination Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `limit` | integer | `20` | Number of items to return (minimum: 1) |
| `offset` | integer | `0` | Number of items to skip |

Applied to `GET /articles`, `GET /articles/feed`.

### Article Query Filters

| Parameter | Type | Description |
|---|---|---|
| `tag` | string | Filter by tag name |
| `author` | string | Filter by author username |
| `favorited` | string | Filter by user who favorited (username) |

## 4. Deployment & Scaling

### Documentation Site Deployment

The documentation site builds to static files:

```bash
cd docs
npm run build
```

```text
astro check && astro build
```

Output goes to `docs/dist/`. Deploy to any static hosting (Netlify, Vercel, GitHub Pages, S3/CloudFront).

> [!TIP]
> The `docs/README.md` includes one-click deploy buttons for Netlify and Vercel using the Starlight template.

### CI Integration

Add API compliance checks to your implementation's CI pipeline:

```yaml
# GitHub Actions example
- name: Run RealWorld API compliance tests
  run: |
    HOST=http://localhost:3000 bash specs/api/run-api-tests-hurl.sh
```

For E2E tests in CI:

```yaml
- name: Run E2E tests
  env:
    CI: true
    API_BASE: http://localhost:3000/api
  run: npx playwright test
```

> [!NOTE]
> Setting `CI=true` enables `forbidOnly: true` (fails on `test.only()` left in code) and increases retries from 1 to 2.

### Bruno Collection CI Check

```yaml
- name: Verify Bruno collection in sync
  run: bun specs/api/hurl-to-bruno.js --check
```

## 5. Operations

### Test Suite Maintenance

#### Adding New API Endpoints

1. Add the endpoint to `specs/api/openapi.yml`
2. Create or extend the appropriate `specs/api/hurl/*.hurl` test file
3. Regenerate Bruno: `bun specs/api/hurl-to-bruno.js`
4. Verify: `bun specs/api/hurl-to-bruno.js --check`

#### Adding New E2E Tests

1. Create or extend a `specs/e2e/*.spec.ts` file
2. Use existing helpers from `specs/e2e/helpers/` for common operations
3. Follow the established pattern: `test.beforeEach` for user registration, `test.afterEach` for context cleanup with cooldown delay

> [!TIP]
> The 500ms–1000ms cooldown in `afterEach` hooks prevents resource exhaustion when running 6+ sequential tests. Always include `await context.close()` followed by a delay.

#### Updating the Hurl-to-Bruno Converter

The converter at `specs/api/hurl-to-bruno.js` handles these Hurl assertion types:

| Hurl Assertion | Bruno/Chai Translation |
|---|---|
| `jsonpath "$.x" == val` | `expect(res.body.x).to.eql(val)` |
| `jsonpath "$.x" != val` | `expect(res.body.x).to.not.eql(val)` |
| `jsonpath "$.x" >= val` | `expect(res.body.x).to.be.at.least(val)` |
| `jsonpath "$.x" isString` | `expect(typeof res.body.x).to.eql("string")` |
| `jsonpath "$.x" isInteger` | `expect(Number.isInteger(res.body.x)).to.eql(true)` |
| `jsonpath "$.x" isBoolean` | `expect(typeof res.body.x).to.eql("boolean")` |
| `jsonpath "$.x" isCollection` | `expect(typeof res.body.x).to.eql("object")` |
| `jsonpath "$.x" isList` | `expect(Array.isArray(res.body.x)).to.eql(true)` |
| `jsonpath "$.x" isObject` | Non-null object + not array |
| `jsonpath "$.x" contains val` | `expect(res.body.x).to.include(val)` |
| `jsonpath "$.x" matches "re"` | `expect(res.body.x).to.match(/re/)` |
| `jsonpath "$.x" not exists` | `expect(parent).to.not.have.property("x")` |
| `jsonpath "$.x" not isEmpty` | `expect(res.body.x).to.not.eql("")` |
| `jsonpath "$.x" count == N` | `expect(res.body.x.length).to.eql(N)` |

Unsupported assertion types throw `Error: Unhandled hurl assert`.

### Monitoring

There is no runtime application to monitor. The key operational concerns are:

- **Test suite health:** Run the full Hurl + E2E suite against the demo backend (`https://api.realworld.show/api`) periodically to catch spec regressions
- **Bruno sync:** Use `--check` mode in CI to prevent drift between Hurl sources and the Bruno collection
- **Documentation site:** The Astro build fails fast on TypeScript errors (`astro check` runs before `astro build`)

## 6. Performance

### Test Execution Timing

The Hurl test suite runs sequentially (`--jobs 1`) because tests depend on state created by earlier tests within each file (e.g., register → login → create article → verify). There is no parallelism path for API tests.

Playwright E2E tests run with `workers: 1` for the same reason. The `afterEach` cooldown delays (500ms–1000ms) add overhead but prevent flaky failures:

| Config Setting | Value | Rationale |
|---|---|---|
| `workers` | `1` | Shared backend state between tests |
| `fullyParallel` | `false` | Test ordering matters within describe blocks |
| `timeout` | `15,000ms` | Per-test timeout |
| `actionTimeout` | `5,000ms` | UI interaction timeout |
| `navigationTimeout` | `10,000ms` | Page load timeout |
| `retries` | `1` (local), `2` (CI) | Compensate for timing-dependent failures |

> [!NOTE]
> The `afterEach` cooldown pattern is documented in source comments as a known workaround: "This timing issue manifests as timeouts when loading article pages. This will be investigated and fixed later."

### Hurl-to-Bruno Converter Performance

The converter processes all 14 Hurl files in a single pass. In `--check` mode, it generates to a temp directory, compares file-by-file, and cleans up. The temp directory is always removed in a `finally` block.

## 7. Security

### API Authentication Requirements

The specification defines security boundaries:

- **Public endpoints** (no auth): `GET /articles`, `GET /articles/{slug}`, `GET /profiles/{username}`, `GET /tags`, `POST /users`, `POST /users/login`
- **Auth-required endpoints** (Token header): All write operations (create, update, delete) and `GET /user`, `GET /articles/feed`
- **Auth-optional endpoints**: `GET /articles` (adds `favorited` context), `GET /profiles/{username}` (adds `following` flag)

The error test suites (`errors_auth.hurl`, `errors_authorization.hurl`) validate:

- Missing token returns 401 for protected endpoints
- Cross-user article update/delete returns 403
- Cross-user comment deletion returns 403
- Unknown resource access returns 404

### XSS Security Test Coverage

The E2E suite includes smoke-level XSS security tests (`xss-security.spec.ts`) that inject malicious payloads **directly via API** (bypassing UI sanitization) to test frontend resilience:

**Image URL injection payloads tested:**
- `onerror` attribute injection
- `onload` attribute injection
- `javascript:` protocol in `src`
- `data:` URI with script content

**Markdown body payloads tested:**
- `<script>` tags
- `<img onerror>` tags
- `<svg onload>` tags
- `<iframe srcdoc>` tags
- `<a href="javascript:">` links
- `<div onmouseover>` event handlers

**Verification method:** A dialog listener (`page.on('dialog')`) detects if any `alert()` fires, plus DOM checks for dangerous attributes (`onerror`, `onload`, `onmouseover`, `onclick`, `onfocus`) and injected `<script>` elements.

> [!CAUTION]
> The XSS tests are explicitly documented as "BASIC SMOKE TESTS ONLY" — they are not a comprehensive security audit. The test file recommends professional security audits, OWASP ZAP/Burp Suite scanning, and CSP headers for production applications.

### Frontend Error Handling Specification

The E2E suite defines expected frontend behavior for error conditions:

| Error Type | Expected Behavior |
|---|---|
| 4XX on `/api/user` (init) | Logout: clear token, show unauthenticated UI |
| 5XX on `/api/user` (init) | "Unavailable" mode: keep token, show reconnect option |
| Network error on `/api/user` | "Unavailable" mode: keep token, show "Connecting" |
| 401 on form submission | Show `.error-messages`, keep form usable |
| 403 on modification | Show `.error-messages`, revert UI state |
| 500 on data fetch | Don't crash, show navbar/banner, degrade gracefully |
| Network error on form | Show "Unable to connect" in `.error-messages` |
| Malformed JSON response | Don't crash, show navbar/banner |

> [!WARNING]
> The `user-fetch-errors.spec.ts` tests explicitly validate that 5XX errors on `/api/user` preserve the JWT token in localStorage. Frontends that clear the token on server errors will fail these tests — the distinction between "your token is bad" (4XX) and "the server is down" (5XX) is enforced.

## 8. Developer Internals

### Repository Structure

```text
realworld/
├── README.md                           # Project overview
├── CONTRIBUTING.md                     # Contribution guidelines
├── LICENSE                             # MIT license
├── Makefile                            # Build automation
├── assets/
│   ├── media/
│   │   ├── conduit-logo.svg            # Generated logo
│   │   ├── conduit-logo.svg.generate.ts # Logo generator (Bun + OpenType.js)
│   │   ├── default-avatar.svg          # Default user avatar
│   │   ├── realworld-logo.{png,svg}    # RealWorld branding
│   │   ├── realworld-dual-mode.png     # Light/dark mode screenshot
│   │   └── mobile_icons/              # iOS, Android, watchOS, iMessage icons
│   └── theme/
│       └── styles.css                  # Shared Conduit CSS theme
├── docs/                               # Astro/Starlight documentation site
│   ├── package.json                    # Standalone deps (astro, starlight, react, tailwind)
│   ├── astro.config.mjs                # Site configuration
│   ├── tsconfig.json                   # TypeScript config (strict + React JSX)
│   ├── src/
│   │   ├── env.d.ts                    # Astro type declarations
│   │   ├── tailwind.css                # Tailwind entry point
│   │   ├── content/
│   │   │   ├── config.ts               # Astro content collection (docsSchema)
│   │   │   └── docs/                   # Specification content (md/mdx)
│   │   │       ├── index.mdx           # Landing page
│   │   │       ├── introduction.mdx
│   │   │       ├── specifications/     # Backend, frontend, mobile specs
│   │   │       ├── implementation-creation/ # How to build an impl
│   │   │       └── community/          # Authors, resources, thanks
│   │   └── assets/img/                 # Site images
│   └── public/
│       └── favicon.svg
├── specs/
│   ├── api/
│   │   ├── openapi.yml                 # OpenAPI 3.1 specification
│   │   ├── hurl-to-bruno.js            # Converter (Bun, 597 lines)
│   │   ├── run-api-tests-hurl.sh       # Hurl test runner
│   │   ├── run-api-tests-bruno.sh      # Bruno test runner
│   │   ├── hurl/                       # 14 Hurl test files
│   │   │   ├── auth.hurl
│   │   │   ├── articles.hurl
│   │   │   ├── comments.hurl
│   │   │   ├── favorites.hurl
│   │   │   ├── feed.hurl
│   │   │   ├── profiles.hurl
│   │   │   ├── tags.hurl
│   │   │   ├── pagination.hurl
│   │   │   ├── errors_auth.hurl
│   │   │   ├── errors_articles.hurl
│   │   │   ├── errors_authorization.hurl
│   │   │   ├── errors_comments.hurl
│   │   │   ├── errors_profiles.hurl
│   │   │   └── run-hurl-tests.sh       # Alternative runner (inside hurl dir)
│   │   └── bruno/                      # Generated Bruno collection
│   │       ├── bruno.json
│   │       ├── collection.bru
│   │       ├── environments/local.bru
│   │       ├── auth/                   # 20 test files
│   │       ├── articles/               # 17 test files
│   │       ├── comments/               # 13 test files
│   │       ├── favorites/              # 9 test files
│   │       ├── feed/                   # 12 test files
│   │       ├── profiles/               # 7 test files
│   │       ├── tags/                   # 4 test files
│   │       ├── pagination/             # 7 test files
│   │       ├── errors-auth/            # 15 test files
│   │       ├── errors-articles/        # 20 test files
│   │       ├── errors-authorization/   # 9 test files
│   │       ├── errors-comments/        # 10 test files
│   │       └── errors-profiles/        # 6 test files
│   └── e2e/
│       ├── playwright.base.ts          # Base Playwright config
│       ├── SELECTORS.md                # CSS selector documentation
│       ├── auth.spec.ts                # Authentication tests (8)
│       ├── articles.spec.ts            # Article CRUD tests (10)
│       ├── comments.spec.ts            # Comment tests (8)
│       ├── navigation.spec.ts          # Navigation/filtering tests (9)
│       ├── social.spec.ts              # Follow/profile tests (5)
│       ├── settings.spec.ts            # Settings form tests
│       ├── error-handling.spec.ts       # Error resilience tests (18)
│       ├── user-fetch-errors.spec.ts    # Init error tests (12)
│       ├── null-fields.spec.ts         # Nullable field tests (11)
│       ├── url-navigation.spec.ts      # URL routing tests (7)
│       ├── health.spec.ts              # Smoke tests (4)
│       ├── xss-security.spec.ts        # XSS smoke tests (12)
│       └── helpers/
│           ├── api.ts                  # Direct API helpers (register, login, create article, update user, bulk create)
│           ├── articles.ts             # UI article helpers (create, edit, delete, favorite, unfavorite, generate)
│           ├── auth.ts                 # UI auth helpers (register, login, logout, generateUniqueUser)
│           ├── comments.ts             # UI comment helpers (add, delete, count)
│           ├── config.ts               # Environment config (API_MODE, API_BASE)
│           ├── debug.ts                # Debug interface helpers (getToken, getAuthState, getCurrentUser)
│           ├── profile.ts              # UI profile helpers (follow, unfollow, updateProfile)
│           └── setup.ts                # Setup utilities (createUserInIsolation, createManyArticles)
```

### Module Reference

#### `specs/api/hurl-to-bruno.js` (597 lines)

The core conversion tool. Written as a Bun executable (`#!/usr/bin/env bun`), it performs a full pipeline:

**Parsing (`parseHurlFile()`):** State machine with 6 states (`IDLE`, `HEADERS`, `BODY`, `RESPONSE`, `ASSERTS`, `CAPTURES`). Handles:
- Comment lines as request names
- HTTP methods (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`)
- Header parsing (`Key: Value`)
- JSON body extraction with brace-depth tracking
- `HTTP NNN` status assertions
- `[Asserts]` section with jsonpath expressions
- `[Captures]` section for variable extraction

**Transformation:** Converts Hurl concepts to Bruno equivalents:
- `slugify()` — generates filesystem-safe names from comments
- `jsonpathToJs()` — `$.user.token` → `res.body.user.token`
- `transformValue()` — handles `null`, booleans, numbers, `{{var}}` templates, quoted strings with embedded variables
- `assertToJs()` — 14 assertion types mapped to Chai `expect()` chains

**Generation (`generateBruFile()`):** Produces `.bru` format sections:
- `meta {}` — name and sequence number
- `METHOD {}` — URL and body type
- `headers {}` — request headers
- `body:json {}` — indented JSON body (2-space indent to avoid parser ambiguity)
- `assert {}` — status code check
- `script:post-response {}` — variable captures + Chai assertions

**Collection structure:** `generateCollection()` creates:
- `bruno.json` — collection metadata
- `collection.bru` — pre-request script generating unique `uid`
- `environments/local.bru` — default host variable
- One folder per Hurl file, one `.bru` file per request

**Check mode (`checkMode()`):** Generates to temp directory, compares file-by-file (missing, extra, changed), exits non-zero on drift.

#### `specs/e2e/helpers/config.ts` (2 lines)

Resolves two environment variables:
- `API_MODE`: `true` unless `process.env.API_MODE` is explicitly `"false"` (case-insensitive)
- `API_BASE`: defaults to `https://api.realworld.show/api`

#### `specs/e2e/helpers/api.ts` (108 lines)

Direct API helpers for test setup, bypassing the UI for speed:
- `registerUserViaAPI()` — `POST /users`, returns JWT token
- `loginUserViaAPI()` — `POST /users/login`, returns JWT token
- `createArticleViaAPI()` — `POST /articles`, returns article slug
- `updateUserViaAPI()` — `PUT /user`
- `createManyArticles()` — bulk article creation with 100ms delays between requests

#### `specs/e2e/helpers/auth.ts` (53 lines)

UI-based authentication flows:
- `register()` — fills form at `/register`, waits for redirect to `/`
- `login()` — fills form at `/login`, waits for redirect to `/`
- `logout()` — navigates to `/settings`, clicks logout button
- `generateUniqueUser()` — returns `{ username, email, password }` with `Date.now()` uniqueness

#### `specs/e2e/helpers/articles.ts` (93 lines)

UI-based article operations:
- `createArticle()` — fills editor form, waits for article page redirect
- `editArticle()` — navigates to `/editor/{slug}`, waits for form population, updates fields
- `deleteArticle()` — clicks delete button, waits for home redirect
- `favoriteArticle()` / `unfavoriteArticle()` — toggles favorite button state
- `generateUniqueArticle()` — returns article data with timestamp uniqueness

#### `specs/e2e/helpers/comments.ts` (33 lines)

UI-based comment operations:
- `addComment()` — fills comment textarea, clicks Post, waits for comment count to increase
- `deleteComment()` — finds comment card by text, clicks trash icon
- `getCommentCount()` — counts `.card:not(.comment-form) .card-block` elements

#### `specs/e2e/helpers/debug.ts` (67 lines)

Interface to `window.__conduit_debug__`:
- `getToken()` — retrieves JWT from debug interface
- `getAuthState()` — returns `authenticated | unauthenticated | unavailable | loading`
- `getCurrentUser()` — returns user object or null
- `waitForAuthState()` — polls until expected state is reached
- `isDebugInterfaceAvailable()` — checks if implementation exposes the interface

#### `specs/e2e/helpers/profile.ts` (60 lines)

UI-based profile operations:
- `followUser()` — navigates to `/profile/{username}`, clicks Follow, waits for Unfollow
- `unfollowUser()` — navigates to `/profile/{username}`, clicks Unfollow, waits for Follow
- `updateProfile()` — fills settings form, handles both API_MODE and form-only submissions

#### `specs/e2e/helpers/setup.ts` (46 lines)

High-level setup utilities:
- `createUserInIsolation()` — creates user in a separate browser context (for non-API mode)
- `createManyArticles()` — dispatches to API or form-based creation depending on `API_MODE`

#### `assets/media/conduit-logo.svg.generate.ts` (41 lines)

Bun script that:
1. Downloads Caudex Bold font from Google Fonts to `.tmp/`
2. Loads font with `opentype.js`
3. Renders "Conduit" text at 48px to SVG path data
4. Computes tight bounding box with 2px padding
5. Writes `conduit-logo.svg` with `fill="#222"`

### Code Patterns and Conventions

**State machine parsing:** The Hurl parser uses a state machine pattern (`IDLE` → `HEADERS` → `BODY` → `RESPONSE` → `ASSERTS` → `CAPTURES`) rather than a grammar parser. This is brittle for complex Hurl features but sufficient for the jsonpath-centric test files in this repo.

**Test isolation:** Every E2E test creates its own user via `generateUniqueUser()` using `Date.now()` for uniqueness. Browser contexts are explicitly closed in `afterEach` with cooldown delays.

**Dual-mode tests:** E2E tests check `API_MODE` to determine whether to use direct API calls (faster) or form-based UI flows (more realistic). API-only tests are skipped with `test.skip(!API_MODE, 'reason')`.

**Variable capture chain:** Hurl tests use `[Captures]` to extract values (tokens, slugs, comment IDs) that subsequent requests use via `{{variable}}` interpolation. The Bruno converter maps these to `bru.setVar()` / `bru.getVar()`.

**Pre-request UID generation:** The Bruno collection-level `script:pre-request` generates a unique `uid` combining `Date.now()` with random characters, ensuring test isolation per collection run.

### Testing Strategy

The repository has three test layers, each targeting different aspects:

| Layer | Tool | Target | Count |
|---|---|---|---|
| API Compliance | Hurl | Backend API correctness | 14 test files, ~150 requests |
| API Compliance (Alt) | Bruno | Same as Hurl (generated) | 149 `.bru` files |
| Frontend E2E | Playwright | Frontend behavior + error handling | 13 spec files, 80+ tests |

**API test coverage areas:** Registration/login, article CRUD, comments, favorites, feeds, profiles, tags, pagination, error codes (400/401/403/404/409/422), authorization boundaries, nullable field normalization.

**E2E test coverage areas:** All API areas plus: session persistence, token invalidation recovery, XSS prevention, URL-based routing, default avatar rendering, null field display, network error resilience, HTTP status code flexibility (200 vs 204 for DELETE).

### Known Issues from Community

| Issue | Status | Impact |
|---|---|---|
| #1584 | Open | Registration accepts invalid emails — spec doesn't mandate server-side email validation |
| #839 | Open | Tag ordering assertion may not match all backend implementations |
| #832 | Open | Legacy Postman regex too strict for valid ISO-8601 timestamps |
| #410 | Closed | Removing all tags requires `tagList: []` (empty array), not `tagList: [""]` |
| #1054 | Closed | Line breaks in article body not rendering correctly in some frontends |
| #681 | Open | `null` password in update user request should be rejected |
| #650 | Open | Non-existent URLs silently redirect to home instead of showing 404 |
| #630 | Open | Settings page requires email/password fields even when unchanged |
| #419 | Open | No forgot-password functionality in the spec |

### Potential Improvements

- **Parallel test execution:** The sequential `--jobs 1` constraint limits test speed. Implementing proper test isolation (unique databases per test run) would enable parallelism.
- **Non-jsonpath assertions:** The hurl-to-bruno converter only handles `jsonpath` assertions. Supporting `header`, `cookie`, and `body` assertions would enable full-fidelity conversion.
- **Mobile specification:** The `docs/src/content/docs/specifications/mobile-specs/` directory exists with an introduction but the mobile spec content is incomplete.
- **Automated E2E test discovery:** Implementations currently must manually copy/symlink the `specs/e2e/` directory. A package distribution model would simplify consumption.
- **Dedicated CSS selector contract:** `specs/e2e/SELECTORS.md` documents the CSS selectors the E2E tests depend on, but there's no automated enforcement that implementations use these exact selectors.

## 9. Integration Points

### For Backend Implementations

Backend implementations must conform to:

1. **OpenAPI spec** at `specs/api/openapi.yml` — all 15 endpoints with exact request/response schemas
2. **Authentication** — JWT tokens with `Token` prefix in Authorization header
3. **Error format** — `{ "errors": { "field": ["message"] } }` envelope
4. **Nullable fields** — `bio` and `image` accept `null` and normalize empty strings to `null`
5. **CORS** — enabled for frontend cross-origin requests (spec mentions CORS in docs)
6. **Status codes** — 201 for creation, 204 for deletion (200 also accepted by frontends), 4XX/5XX per spec

Validate with: `HOST=http://your-server bash specs/api/run-api-tests-hurl.sh`

### For Frontend Implementations

Frontend implementations must:

1. **Route structure** — `/`, `/login`, `/register`, `/editor`, `/editor/{slug}`, `/article/{slug}`, `/profile/{username}`, `/profile/{username}/favorites`, `/settings`, `/tag/{tag}`, `/?feed=following`
2. **CSS classes** — `.navbar`, `.banner`, `.article-preview`, `.article-content`, `.article-meta`, `.feed-toggle`, `.tag-list`, `.tag-pill`, `.tag-default`, `.user-info`, `.user-img`, `.user-pic`, `.comment-form`, `.comment-author-img`, `.error-messages`, `.sidebar`, `.pagination`, `.page-item`, `.empty-feed-message`, `.mod-options`
3. **Form elements** — `input[name="title"]`, `input[name="description"]`, `textarea[name="body"]`, `input[placeholder="Enter tags"]`, `input[name="email"]`, `input[name="password"]`, `input[name="username"]`, `input[name="image"]`, `textarea[name="bio"]`, `textarea[placeholder="Write a comment..."]`
4. **Buttons** — `button:has-text("Publish Article")`, `button:has-text("Delete Article")`, `button:has-text("Follow")`, `button:has-text("Unfollow")`, `button:has-text("Favorite")`, `button:has-text("Unfavorite")`, `button:has-text("Post Comment")`, `button:has-text("Update Settings")`, `button:has-text("Or click here to logout")`
5. **Debug interface** (optional) — `window.__conduit_debug__` with `getToken()`, `getAuthState()`, `getCurrentUser()`
6. **Default avatar** — render `default-avatar.svg` when user image is `null` or empty
7. **Error handling** — 4XX clears token and shows logged-out UI; 5XX preserves token and shows "Connecting" state

Validate with: `API_BASE=http://your-api npx playwright test --config specs/e2e/playwright.base.ts`

### For Documentation Contributors

The docs site uses Astro content collections:

```typescript
// docs/src/content/config.ts
export const collections = {
  docs: defineCollection({ schema: docsSchema() }),
};
```

Add new pages as `.md` or `.mdx` files in `docs/src/content/docs/`. Starlight automatically generates navigation from the file structure. Images go in `docs/src/assets/img/`.

---

_Generated May 2026 from realworld-apps/realworld at e75fef3. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
