Jesse A. Green / notebook

RealWorld (Conduit) — Administrator & Developer Guide

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.

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).

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

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

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)

{
  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):

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:

{
  "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:

cd docs
npm run build
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:

# 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:

- 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

- 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

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:

// 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/.