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_BASEdefaults to the public demo server athttps://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: 1andfullyParallel: falseare 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:
401Unauthorized — missing or invalid token403Forbidden — user lacks permission for the operation404Not Found — resource doesn't exist409Conflict — duplicate resource (username/email)422Unprocessable 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.mdincludes 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=trueenablesforbidOnly: true(fails ontest.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
- Add the endpoint to
specs/api/openapi.yml - Create or extend the appropriate
specs/api/hurl/*.hurltest file - Regenerate Bruno:
bun specs/api/hurl-to-bruno.js - Verify:
bun specs/api/hurl-to-bruno.js --check
Adding New E2E Tests
- Create or extend a
specs/e2e/*.spec.tsfile - Use existing helpers from
specs/e2e/helpers/for common operations - Follow the established pattern:
test.beforeEachfor user registration,test.afterEachfor context cleanup with cooldown delay
Tip
The 500ms–1000ms cooldown in
afterEachhooks prevents resource exhaustion when running 6+ sequential tests. Always includeawait 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
--checkmode in CI to prevent drift between Hurl sources and the Bruno collection - Documentation site: The Astro build fails fast on TypeScript errors (
astro checkruns beforeastro 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
afterEachcooldown 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(addsfavoritedcontext),GET /profiles/{username}(addsfollowingflag)
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:
onerrorattribute injectiononloadattribute injectionjavascript:protocol insrcdata: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.tstests explicitly validate that 5XX errors on/api/userpreserve 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 NNNstatus assertions[Asserts]section with jsonpath expressions[Captures]section for variable extraction
Transformation: Converts Hurl concepts to Bruno equivalents:
slugify()— generates filesystem-safe names from commentsjsonpathToJs()—$.user.token→res.body.user.tokentransformValue()— handlesnull, booleans, numbers,{{var}}templates, quoted strings with embedded variablesassertToJs()— 14 assertion types mapped to Chaiexpect()chains
Generation (generateBruFile()): Produces .bru format sections:
meta {}— name and sequence numberMETHOD {}— URL and body typeheaders {}— request headersbody:json {}— indented JSON body (2-space indent to avoid parser ambiguity)assert {}— status code checkscript:post-response {}— variable captures + Chai assertions
Collection structure: generateCollection() creates:
bruno.json— collection metadatacollection.bru— pre-request script generating uniqueuidenvironments/local.bru— default host variable- One folder per Hurl file, one
.brufile 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:trueunlessprocess.env.API_MODEis explicitly"false"(case-insensitive)API_BASE: defaults tohttps://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 tokenloginUserViaAPI()—POST /users/login, returns JWT tokencreateArticleViaAPI()—POST /articles, returns article slugupdateUserViaAPI()—PUT /usercreateManyArticles()— 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 buttongenerateUniqueUser()— returns{ username, email, password }withDate.now()uniqueness
specs/e2e/helpers/articles.ts (93 lines)
UI-based article operations:
createArticle()— fills editor form, waits for article page redirecteditArticle()— navigates to/editor/{slug}, waits for form population, updates fieldsdeleteArticle()— clicks delete button, waits for home redirectfavoriteArticle()/unfavoriteArticle()— toggles favorite button stategenerateUniqueArticle()— 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 increasedeleteComment()— finds comment card by text, clicks trash icongetCommentCount()— counts.card:not(.comment-form) .card-blockelements
specs/e2e/helpers/debug.ts (67 lines)
Interface to window.__conduit_debug__:
getToken()— retrieves JWT from debug interfacegetAuthState()— returnsauthenticated | unauthenticated | unavailable | loadinggetCurrentUser()— returns user object or nullwaitForAuthState()— polls until expected state is reachedisDebugInterfaceAvailable()— 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 UnfollowunfollowUser()— navigates to/profile/{username}, clicks Unfollow, waits for FollowupdateProfile()— 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 onAPI_MODE
assets/media/conduit-logo.svg.generate.ts (41 lines)
Bun script that:
- Downloads Caudex Bold font from Google Fonts to
.tmp/ - Loads font with
opentype.js - Renders "Conduit" text at 48px to SVG path data
- Computes tight bounding box with 2px padding
- Writes
conduit-logo.svgwithfill="#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 1constraint 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
jsonpathassertions. Supportingheader,cookie, andbodyassertions 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.mddocuments 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:
- OpenAPI spec at
specs/api/openapi.yml— all 15 endpoints with exact request/response schemas - Authentication — JWT tokens with
Tokenprefix in Authorization header - Error format —
{ "errors": { "field": ["message"] } }envelope - Nullable fields —
bioandimageacceptnulland normalize empty strings tonull - CORS — enabled for frontend cross-origin requests (spec mentions CORS in docs)
- 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:
- Route structure —
/,/login,/register,/editor,/editor/{slug},/article/{slug},/profile/{username},/profile/{username}/favorites,/settings,/tag/{tag},/?feed=following - 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 - 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..."] - 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") - Debug interface (optional) —
window.__conduit_debug__withgetToken(),getAuthState(),getCurrentUser() - Default avatar — render
default-avatar.svgwhen user image isnullor empty - 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/.