diff --git a/AGENTS.md b/AGENTS.md index 002fae02d6e..987de33d54b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ Start with: - [Writing codebase documentation](docs/contributing/documentation.md) - [Testing](docs/contributing/testing.md) - [Shipping](docs/contributing/shipping.md) +- [Codebase direction](docs/codebase/direction.md) - [Monorepo structure](docs/codebase/monorepo-structure.md) ## Required workflow diff --git a/docs/README.md b/docs/README.md index 96a9e71adca..75355906c02 100644 --- a/docs/README.md +++ b/docs/README.md @@ -76,6 +76,7 @@ adding translatable product copy, see the Codebase guides explain how the main systems fit together: +- [Codebase direction](codebase/direction.md) - [Runtime architecture](codebase/runtime-architecture.md) - [Authentication](codebase/authentication.md) - [Configuration](codebase/configuration.md) @@ -98,6 +99,8 @@ Practice and contributor guides explain how to make and verify changes: - [Feature flags](practices/feature-flags.md) - [Internationalization](practices/internationalization.md) - [Performance testing](contributing/performance-testing.md) +- [Stripe testing](contributing/testing-stripe.md) +- [Test data](contributing/test-data.md) - [Testing development URLs and devices](contributing/testing-development-urls.md) Reference guides provide tables and other information to look up while working diff --git a/docs/codebase/direction.md b/docs/codebase/direction.md new file mode 100644 index 00000000000..e0b070c2976 --- /dev/null +++ b/docs/codebase/direction.md @@ -0,0 +1,194 @@ +# Codebase direction + +Ghost is evolving incrementally while continuing to ship. Some of the most +common patterns in the repository are legacy patterns rather than examples to +copy into new code. + +Ghost 7.0 is planned for the first half of 2027. The work on this page shapes +the codebase leading up to that release. + +This guide records the direction of the codebase and what contributors should +do now. It is not a roadmap or a promise to migrate everything at once. When a +focused guide exists, follow that guide for implementation details. + +## Main priorities + +Our two most important priorities are: + +1. **React:** move Ghost Admin onto React and off Ember. +2. **Type safety:** move the codebase to TypeScript and validate runtime + boundaries with Zod. + +## Status terms + +- **Active migration** means new work follows the new path while existing code + moves over in coherent pieces. +- **Exploring** means the direction is agreed and work is underway to figure + out the implementation pattern. +- **Planned** means a concrete change is committed, but the migration has not + started yet. + +## Direction at a glance + +| Area | Direction | Status | +| -------------------- | ---------------------------------------------------- | ---------------- | +| Admin UI | Ember to React | Active migration | +| Application code | JavaScript to TypeScript | Active migration | +| Node.js modules | CommonJS to ESM | Active migration | +| Runtime boundaries | Validate unknown data with Zod | Exploring | +| Server dependencies | Inject stateful dependencies | Exploring | +| Data access | Bookshelf to services, repositories, and Knex | Exploring | +| Server state | Interchangeable, stateless instances | Active migration | +| Repository layout | Consolidate related projects into the monorepo | Active migration | +| Development patterns | Establish golden paths for recurring work | Exploring | +| Database support | Remove SQLite support in Ghost 7.0 | Planned | +| Editor content | Remove Mobiledoc support in Ghost 7.0 | Planned | +| Self-hosting | Deprecate Ghost-CLI in favour of Docker in Ghost 7.0 | Active migration | +| Node.js runtime | Keep pace with Node Current | Planned | +| Authentication | Standards-based auth built on Better Auth | Exploring | +| Linting | ESLint to Oxlint | Planned | + +## Architectural direction + +The following decisions give new work a review direction without pretending +that every migration path is settled: + +- **Stateless Ghost:** application instances for a site should be + interchangeable. New work should reduce reliance on local files, + process-local state, and boot-time snapshots. +- **Dependency injection:** modules should be handed the stateful things they + use. Wiring moves toward the edge; this does not require a DI framework or + prohibit ordinary imports. +- **Type safety:** TypeScript is the language direction and Zod is the runtime + boundary direction. Exact schema ownership and sharing patterns still need + golden paths where the codebase has no established answer. +- **Golden paths:** recurring work should have one obvious, supported route + embodied in code, templates, tooling, tests, documentation, and agent + guidance. Laravel is a reference for the quality and completeness of that + experience, not a framework to copy. +- **Modern authentication:** authentication should converge on standard + protocols and credential lifecycles, with Better Auth as the intended + foundation. Existing staff, member, integration, and Content API mechanisms + remain the current contract until replacements are implemented and migrated. + Follow the [authentication guide](authentication.md) for current behavior. + +## Guidance for new work + +### Build Admin features in React + +Build new Admin UI in [`apps/admin/`](../../apps/admin/) with +`admin-x-framework` for API access and Shade for UI. Do not add a new Ember +route or use Ember merely because an older version of the feature does. + +Migrate an existing Ember feature at a coherent product boundary. React and +Ember still ship together, so preserve navigation, authentication, shared +state, and older-server behavior across the bridge. The +[Admin README](../../apps/admin/README.md) describes the current integration. + +### Use TypeScript + +Write new product code in TypeScript where the surrounding runtime supports +it. Use types to model the domain rather than replacing uncertainty with +`any`, unchecked assertions, or `@ts-nocheck`. + +Use your judgement when deciding whether to convert existing files. A small, +unrelated change may not justify a migration. When working substantially in an +area, take the opportunity to migrate it where feasible. Prefer converting a +coherent module or directory together, with its tests, rather than leaving a +mixture of JavaScript and TypeScript. Preserve the behavior of callers that +have not yet migrated. + +### Use ESM at supported boundaries + +New internal packages are TypeScript-only ESM packages. Follow the +[internal package golden path](../../packages/README.md) rather than adding a +CommonJS build by default. + +Ghost Core still contains CommonJS entry points and consumers. New TypeScript +services can use ESM internally while retaining a thin CommonJS wrapper where +an existing `require()` boundary needs one. Do not convert a public package or +established runtime boundary without checking its consumer and release +contract. + +### Validate runtime boundaries + +TypeScript cannot prove the shape of data arriving over HTTP, from the +database, configuration, files, queues, or third-party services. Treat that +data as `unknown` until it has been validated. Zod is the preferred runtime +schema and validation library for new boundaries, and TypeScript types should +be inferred from the schema where practical. + +There is not yet one settled layout for schemas shared across every part of +Ghost. Follow a proven nearby implementation, keep one source of truth for a +shape, and avoid adding competing handwritten validation and type definitions. +Ordinary internal function calls do not need runtime validation when +TypeScript already controls both sides. + +### Put behavior in services and data access in repositories + +For a new server feature, put domain behavior in a TypeScript service. We are +exploring repositories and direct Knex as the replacement for Bookshelf, but +the complete data-access pattern is not settled yet. Do not create a new +Bookshelf model or add new business logic to model lifecycle hooks. + +Existing features still depend heavily on Bookshelf. When working in one, move +behavior behind an explicit service or repository seam before replacing its +persistence. Do not bypass existing behavior simply to avoid the model. + +Pass stateful dependencies such as database connections, models, caches, +configuration, and I/O services into new modules. Construct and connect them at +the application edge. Pure functions, constants, and types can still be +imported normally; dependency injection does not require a container. Follow +the [services guide](../../ghost/core/core/server/services/README.md) for the +current construction and initialization pattern. + +### Avoid new process-local state + +Design new server behavior so any Ghost instance for a site can serve the next +request. Do not make local files, startup-only precomputation, module singletons, +or uncoordinated in-memory state the source of truth. + +An in-memory cache can still be appropriate when it can be rebuilt from a +shared source and does not require instances to synchronize. The practical +test is whether restarting or switching the serving instance loses data or +breaks behavior. See the [runtime architecture](runtime-architecture.md) and +[internal caching](internal-caching.md) guides for the current boundaries. + +## Compatibility and infrastructure transitions + +### Ghost 7.0 + +Ghost 7.0 is planned to deprecate Ghost-CLI in favour of Docker for +self-hosting, and to remove support for Mobiledoc and SQLite. Until then, +preserve the existing contracts where they are still supported, but do not +build new features around them. + +### SQLite + +Ghost currently supports SQLite through `better-sqlite3`, with the old +`sqlite3` configuration name retained for compatibility. SQLite support is +planned to be removed in Ghost 7.0. Do not add new SQLite-specific behavior or +assume SQLite will remain a supported production database. + +### Node.js + +Ghost currently supports Node.js 22 and 24. CI tests both supported lines, and +new code and dependencies must work on both. The longer-term direction is to +keep pace with Node Current, but dropping an existing version is an explicit +compatibility and release decision. + +Check the [Node.js compatibility table](../reference/node-compatibility.md) +instead of inferring support from the version installed locally. + +## Working in transitional code + +- Do not assume the most numerous pattern is the preferred pattern. +- Do not expand a legacy dependency when a supported new path exists. +- Migrate a coherent boundary, including its tests and compatibility behavior, + rather than mixing broad cleanup into an unrelated change. +- Preserve old and new paths where an incremental migration requires both. +- Treat an agreed direction as a design constraint, not permission to invent a + local framework. If the implementation pattern is unclear, establish it + before copying it across the codebase. +- Update this guide when work moves between stages, a migration completes, or a + planned tool becomes authoritative. diff --git a/docs/codebase/stripe-flows.md b/docs/codebase/stripe-flows.md index bec059974ca..355c63c4908 100644 --- a/docs/codebase/stripe-flows.md +++ b/docs/codebase/stripe-flows.md @@ -36,3 +36,6 @@ This flow is implemented in The checkout and tier price flows are implemented by [`payments-service.js`](../../ghost/core/core/server/services/members/members-api/services/payments-service.js). + +For manual and automated development workflows, see +[Testing Stripe locally](../contributing/testing-stripe.md). diff --git a/docs/contributing/development-setup.md b/docs/contributing/development-setup.md index 3bdcc1b7727..44ac5de559a 100644 --- a/docs/contributing/development-setup.md +++ b/docs/contributing/development-setup.md @@ -113,7 +113,8 @@ environment and adds the listed tooling: | `pnpm dev:analytics` | Tinybird-backed analytics with the latest published version of the Traffic Analytics service | | `pnpm dev:analytics:local` | Tinybird-backed analytics with your locally running instance of the Traffic Analytics service | | `pnpm dev:storage` | S3-compatible storage through MinIO on ports `9000` and `9001` | -| `pnpm dev:stripe` | Stripe webhooks exactly as production receives them; requires Tailscale, see below | +| `pnpm dev:stripe` | Stripe webhooks exactly as production receives them; see [Stripe testing](testing-stripe.md) | +| `pnpm dev:mailgun` | Mailgun API delivery; see [email testing](testing-email.md) | | `pnpm dev:full` | Public app watchers plus analytics, storage, and Stripe | Copy [`.env.example`](../../.env.example) to `.env` only when you need an @@ -123,38 +124,6 @@ To open Ghost on a phone or another computer, or to exercise HTTPS, subdirectory, and separate-Admin URL behaviour, see [Testing development URLs and devices](testing-development-urls.md). -### Stripe webhooks - -`pnpm dev:stripe` runs the webhook path production runs. It publishes Ghost's -webhook route, and nothing else, through -[Tailscale Funnel](https://tailscale.com/kb/1223/funnel), and Ghost registers a -pinned webhook endpoint at that address once Stripe is connected in Admin, then -deletes it on shutdown. -The site and Admin stay on `localhost`, so hot reload and the rest of the -development environment work as usual. Use it when the shape of a webhook -payload matters, for example when reading new fields from a checkout session. -Ghost logs an error whenever an event arrives rendered at a different API -version from the one it pins, in any environment. - -The webhook route is reachable from the internet while the command runs; every -request to it must carry a valid Stripe signature. The tunnel is a child -process of the command and ends with it, including on Ctrl-C. Only a forced -kill of the command can leave the tunnel running, and even then it does not -survive a restart of Tailscale or the machine. - -Funnel needs Tailscale 1.52 or newer with MagicDNS, HTTPS certificates and -Funnel enabled for your tailnet and node. The command reports when Tailscale is -missing, not signed in, or has no MagicDNS name; for the other requirements it -shows Tailscale's own error. - -`pnpm dev:stripe --listen` forwards events with `stripe listen` instead, which -needs `STRIPE_SECRET_KEY` in the environment or a local `.env` file but no -Tailscale. The CLI renders every event at your Stripe account's default API -version, which cannot be pinned, so an event can carry a different shape from -the one production receives; the command warns about this at startup and Ghost -logs an error when a mismatched event arrives. Use it only when the payload -shape does not matter. - ## Data and email After creating the local owner account, populate a development site with stable @@ -167,7 +136,8 @@ pnpm reset:data This clears the development database while preserving the owner, then creates 1,000 members and 100 posts. Use `pnpm reset:data:empty` for an empty site. Both commands are destructive and require the Docker development environment to be -running. +running. See [Working with test data](test-data.md) for larger and custom +datasets. When developing a database migration, apply pending migrations to the running development database with: @@ -177,7 +147,8 @@ pnpm migrate:db ``` Development email is captured by Mailpit rather than delivered. Open -[http://localhost:8025](http://localhost:8025) to inspect messages. +[http://localhost:8025](http://localhost:8025) to inspect messages. For Mailgun +delivery and automated-test workflows, see [Email testing](testing-email.md). ## Updating and recovering diff --git a/docs/contributing/test-data.md b/docs/contributing/test-data.md new file mode 100644 index 00000000000..ef7c21d9355 --- /dev/null +++ b/docs/contributing/test-data.md @@ -0,0 +1,64 @@ +# Working with Test Data + +Ghost includes a data generator for building repeatable local datasets. Use it +instead of copying data from a real publication. + +## Reset the Development Site + +From the repository root, run: + +```bash +pnpm reset:data +``` + +This clears generated data from the Docker development database, preserves the +owner account, and creates 1,000 members and 100 posts using a fixed seed. + +Other prepared datasets are available: + +```bash +pnpm reset:data:empty +pnpm reset:data:xxl +``` + +`reset:data:empty` keeps the owner but generates no members or posts. +`reset:data:xxl` creates two million members for testing behaviour at scale. + +These commands are destructive and require the Docker development environment +to be running. Do not point the generator at a database containing data you +need to keep. Restart `pnpm dev` after resetting data so running processes do +not retain state from the old dataset. + +## Generate a Custom Dataset + +Run the generator inside the development container when the prepared datasets +do not cover the scenario: + +```bash +docker exec ghost-dev bash -c \ + 'cd /home/ghost/ghost/core && node index.js generate-data \ + --clear-database --quantities members:10000,posts:500 --seed 123' +``` + +The generator supports: + +- `--clear-database` to clear the tables being generated while preserving the + owner account; +- `--tables=members:10000,posts:500` to generate only named tables and their + dependencies, with optional quantities; +- `--with-default` to add the other default tables when using `--tables`; +- `--quantities=members:10000,posts:500` to override quantities without + changing which default tables are generated; +- `--base-data-pack=/path/to/data.json` to import compatible newsletters, + posts, tags, products, settings, and custom theme settings before generating + the remaining tables. Importing a base pack replaces the existing settings; +- `--seed=123` to make generated values repeatable. Timestamps can still move + so that generated content remains current; +- `--print-dependencies` to show the table dependency order without importing. + +Use `--tables` for a narrow dataset and `--quantities` when the relationships +from the full default dataset matter. The generator adds required table +dependencies automatically and rejects unknown table names. + +For the implementation and instructions for adding an importer, see the +[data generator README](../../ghost/core/core/server/data/seeders/README.md). diff --git a/docs/contributing/testing-email.md b/docs/contributing/testing-email.md index ad6ee3204d0..7b17acf95a4 100644 --- a/docs/contributing/testing-email.md +++ b/docs/contributing/testing-email.md @@ -1,6 +1,6 @@ # Receiving and Testing Emails -## Local email +## Use Mailpit by default The normal development environment starts Mailpit with Ghost. Run: @@ -8,18 +8,54 @@ The normal development environment starts Mailpit with Ghost. Run: pnpm dev ``` -Emails sent by the development site are captured at +Transactional emails sent by the development site are captured at [http://localhost:8025](http://localhost:8025) rather than delivered. The Docker development configuration connects Ghost to Mailpit automatically. -## Testing with Mailgun +Use Mailpit for ordinary local work. It is quick, keeps test messages on your +machine, and does not require provider credentials. It does not exercise the +Mailgun API used for newsletters and other bulk email. -For testing transactional email delivery, configure Ghost's `mail` setting with -an SMTP provider. For testing newsletter delivery, configure the separate -Mailgun settings used by Ghost's bulk email service. Mailgun sandbox domains -only send to recipients that have been added and verified in Mailgun. +## Test with Mailgun -Keep credentials in your local configuration and do not commit them. +Use the Mailgun development variant when the provider interaction is part of +the behaviour you need to test. It routes transactional, newsletter, and +automation email through the Mailgun API: -Most development does not need real delivery. Use Mailpit unless the behavior -being tested depends on the external provider. +```bash +pnpm dev:mailgun +``` + +Copy [`.env.example`](../../.env.example) to `.env` and provide a test Mailgun +domain and API key. The example also documents the optional sender and the +different API URLs required by EU domains. Never commit `.env` or provider +credentials. + +The development variant supplies Ghost's Mailgun configuration for you. Do not +add SMTP credentials or Mailgun settings to `config.local.json`. + +If you use a Mailgun sandbox domain, add and verify each intended recipient in +Mailgun before testing delivery. + +This sends real email through an external service. Use test addresses and the +smallest useful recipient list. Return to `pnpm dev` when provider behaviour is +not under test. + +## Automated tests + +Automated tests must not call the real Mailgun API. Browser E2E tests can enable +the suite's fake Mailgun service: + +```ts +test.use({mailgunEnabled: true}); +``` + +The fake service records Mailgun requests and forwards rendered messages to +Mailpit, where tests can inspect them with the existing email fixture. See the +[E2E workspace README](../../e2e/README.md) and the +[newsletter-send test](../../e2e/tests/admin/posts/newsletter-send.test.ts) for +the current fixtures and an example. + +Ghost Core tests should use the existing Mailgun stubs and email test utilities +instead of provider credentials. Start with the [testing guide](testing.md) to +choose the suite closest to the behaviour being changed. diff --git a/docs/contributing/testing-stripe.md b/docs/contributing/testing-stripe.md new file mode 100644 index 00000000000..4ec3dd02316 --- /dev/null +++ b/docs/contributing/testing-stripe.md @@ -0,0 +1,85 @@ +# Testing Stripe Locally + +Use a Stripe test-mode account and Stripe's test payment details for all local +development. Never use live keys or real payment details. + +## Receive production-shaped webhooks + +Run: + +```bash +pnpm dev:stripe +``` + +This follows the production webhook path. It publishes only Ghost's Stripe +webhook route through [Tailscale Funnel](https://tailscale.com/kb/1223/funnel). +Once Stripe is connected in Admin, Ghost registers a temporary webhook endpoint +using its pinned Stripe API version and removes it on shutdown. The site and +Admin remain on `localhost`. + +Use this mode when webhook payload shape matters, such as when reading fields +from a checkout session. Ghost logs an error if an event arrives at a different +API version from the one it pins. + +Funnel requires Tailscale 1.52 or newer, with MagicDNS, HTTPS certificates, and +Funnel enabled for the tailnet and node. The command reports when Tailscale is +missing, disconnected, or has no MagicDNS name. Other setup failures include +Tailscale's own error. + +The webhook route is publicly reachable while the command runs, but every +request must have a valid Stripe signature. The tunnel closes when the command +stops. A forced kill can leave it running until Tailscale or the machine +restarts. Turn it off manually if this happens: + +```bash +tailscale funnel --https=443 off +``` + +## Use the Stripe CLI fallback + +When Tailscale is unavailable and exact webhook payload shape does not matter, +run: + +```bash +pnpm dev:stripe --listen +``` + +This uses `stripe listen` in Docker and requires `STRIPE_SECRET_KEY` in the +environment or a local `.env` file. The key must be a test-mode key for the same +Stripe account connected to Ghost. The command does not require a local Stripe +CLI installation or `stripe login`. Never commit `.env` or Stripe credentials. + +Stripe CLI renders events at the account's default API version rather than the +version Ghost pins. The command warns about this difference, and Ghost logs an +error when it receives a mismatched event. + +## Test a paid membership + +1. Start `pnpm dev:stripe`. +2. Connect a Stripe test-mode account in Ghost Admin under + **Settings → Tiers**. Ghost registers the temporary webhook endpoint when the + connection settings are saved. Follow the development log's instruction to + restart if registration could not happen during the first connection. +3. Sign up for a paid membership through the local site's Portal using a + [Stripe test card](https://docs.stripe.com/testing), such as + `4242 4242 4242 4242` with any future expiry date and any three-digit CVC. +4. Confirm that the member becomes paid in Admin. This verifies that Ghost + received and processed the webhook. + +## Automated tests + +Automated browser tests must use the E2E suite's fake Stripe service rather than +a real account: + +```ts +test.use({stripeEnabled: true}); +``` + +This gives the test an isolated Ghost environment, fake Checkout page, Stripe +test service, and signed webhook delivery. See the +[E2E Stripe fixture guide](../../e2e/README.md#stripe-fixtures) and the +[subscription lifecycle test](../../e2e/tests/public/stripe-webhook-subscription-lifecycle.test.ts) +for the current helpers and an example. + +For the implementation behind Stripe Connect, tier creation, and subscription +checkout, see [Stripe flows](../codebase/stripe-flows.md). diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md index ccb1aa5dd74..c4a85559245 100644 --- a/docs/contributing/testing.md +++ b/docs/contributing/testing.md @@ -45,10 +45,58 @@ When a regression crosses several layers, prefer a focused test at the lowest layer that proves the fix. Add a broader acceptance or browser E2E test when the integration between layers is itself the behavior being protected. +## Write Useful Tests + +- Test observable behaviour rather than private implementation details. A + refactor that preserves behaviour should not require unrelated test changes. +- Use the smallest test boundary that provides confidence. Unit tests should + not boot Ghost or use a database; use an integration or server E2E test when + the database, HTTP boundary, or interaction between modules is the behaviour. +- Set up only the data relevant to the scenario. Prefer existing fixtures and + factories over large shared datasets or dependencies on another test. +- Never call a real external service from an automated test. Use the suite's + existing fake service, mock, or request interceptor and make its expectations + specific enough to catch the wrong method, path, payload, or request count. +- Keep time, randomness, environment, and ordering deterministic. Restore + modified globals, timers, mocks, configuration, and singleton state. +- Assert the result a user or caller depends on, including important side + effects. Avoid assertions that merely repeat how the implementation works. + +Snapshot tests are useful for stable, structured output, but a generated +snapshot is not proof that the output is correct. Review every changed snapshot +before committing it and add focused assertions for behaviour that should be +obvious to a reader. + +## Coverage + +Coverage helps find code that a test never exercises; it does not replace +meaningful assertions. Ghost Core uses Vitest's V8 coverage provider. CI applies +separate Vitest thresholds to the integration and server E2E lanes, then uploads +both under Codecov's single `e2e-tests` project alongside Admin coverage. The +Vitest configuration contains the lane thresholds, provider, and excluded files; +`.github/codecov.yml` contains Codecov's project threshold. + +Do not work towards an assumed repository-wide percentage. Add the tests needed +to protect the changed behaviour and treat an unexpected coverage reduction as +a prompt to inspect what is missing. + +To inspect Ghost Core unit coverage locally, run: + +```bash +cd ghost/core +pnpm test:unit --coverage +``` + +The HTML report is written to `ghost/core/coverage/index.html`. + For physical-device testing and URL configurations such as HTTPS, subdirectories, or a separate Admin origin, see [Testing development URLs and devices](testing-development-urls.md). +For provider-backed development and the test doubles used by browser tests, see +[Email testing](testing-email.md) and [Stripe testing](testing-stripe.md). +For repeatable local datasets, see [Working with test data](test-data.md). + ## Run Focused Tests Nx can run a target for one workspace from the repository root: @@ -137,6 +185,44 @@ isolation, fixtures, and debugging, and [Writing Browser E2E Tests](e2e-testing.md) for test conventions, selectors, and Page Objects. +## Diagnose Flaky Tests + +A test is flaky when the same code and inputs can produce different results. +Do not assume a passing retry means the test is harmless: the nondeterminism can +come from application code as well as the test. + +Common causes include: + +- state shared between tests through a database, file, port, global, mock, or + singleton; +- tests that depend on execution order or data created by another test; +- fixed delays and assertions that run before an asynchronous operation has + reached an observable state; +- uncontrolled time, randomness, timezone, network access, or external + services; +- parallel tests competing for the same resource; and +- a race or other nondeterminism in the application code itself. + +1. Re-run the smallest affected file or test, then run its containing group to + check for leaked state or order dependence. +2. Read the first failure rather than relying on a later retry. Browser E2E + tests have retries disabled and retain a Playwright trace on failure. +3. Reproduce with the same concurrency, timezone, service availability, and + isolation mode as the failing environment where those factors are relevant. +4. Replace fixed delays with an observable state change. Use fake clocks for + time-dependent code and explicit fixtures for random or environment-derived + values. +5. Check that each test owns its data and restores mocks, timers, globals, and + configuration. Cleanup belongs in suite hooks that still run when an + assertion fails. +6. Fix the underlying race or isolation failure. Do not add a retry or increase + a timeout unless the operation is genuinely allowed to take longer. + +For browser failures, use `pnpm test:e2e --debug`, the retained Playwright trace, +or the preserved-environment workflow in the E2E documentation. Ember Admin +tests can temporarily use `await this.pauseTest()` as described in its README. +Remove debugging changes before committing. + ## Run Ember Admin Tests Always run Ember Admin tests through Nx so its dependency graph is built first: diff --git a/ghost/core/core/server/data/seeders/README.md b/ghost/core/core/server/data/seeders/README.md new file mode 100644 index 00000000000..de1228b9a8c --- /dev/null +++ b/ghost/core/core/server/data/seeders/README.md @@ -0,0 +1,48 @@ +# Data Generator + +The development data generator populates related Ghost tables in dependency +order. Its CLI entry point is `node index.js generate-data`; contributors +normally use the root `reset:data` scripts described in the +[test-data guide](../../../../../../docs/contributing/test-data.md). + +## How It Works + +Importers live in `importers/` and each own one table. An importer declares its +default quantity and any dependencies that are not represented by schema +foreign keys. The generator adds schema and declared dependencies, sorts the +tables, generates their records, and calls each importer's `finalise()` method. + +A numeric seed resets Faker for every table. This keeps Faker-generated values +stable when another table is added or omitted. Time-dependent fields may still +vary between runs. Use the provided Faker instances and random-data helpers +rather than `Math.random()` so seeded values remain repeatable. + +## Add or Change an Importer + +- Extend `TableImporter` and register the importer in `importers/index.js`. +- Set a modest `defaultQuantity`; callers can request larger datasets through + `--tables` or `--quantities`. +- Declare dependencies that the database schema cannot supply. The generator + derives normal foreign-key dependencies itself. +- Use `setReferencedModel()` and maps or indexes when records depend on another + generated table. Avoid repeatedly scanning large arrays during generation. +- Generate IDs with `fastFakeObjectId()` rather than Faker's MongoDB ID helper. + The generated IDs are fast and safely earlier than the current time. +- Put derived-table or summary work in `finalise()` so it runs after every + importer has completed. + +## Bulk Inserts + +`TableImporter.batchInsert()` uses Knex for small datasets. Above 5,000 records +it writes CSV chunks and uses MySQL's `LOAD DATA LOCAL INFILE`, unless +`DISABLE_FAST_IMPORT` is set. + +The CLI requires infile streaming. It is enabled automatically in development; +other environments must explicitly set `ALLOW_INFILE_STREAM=1`. Do not broaden +that permission in application configuration: it allows the database client to +read a requested local file. + +The generator temporarily changes MySQL foreign-key, uniqueness, local-infile, +and redo-log settings for fast imports. It re-enables redo logging at the end of +a successful import. When changing this lifecycle, ensure failure paths also +restore any persistent database setting they changed. diff --git a/ghost/core/test/README.md b/ghost/core/test/README.md index 8854ea2b21b..24a47905c0b 100644 --- a/ghost/core/test/README.md +++ b/ghost/core/test/README.md @@ -49,6 +49,18 @@ The webhook mock receiver follows the same pattern for outgoing webhooks. Real network access is disabled when the framework boots. Use a focused mock or an existing `mockManager` helper for external services. +When a test uses Nock directly, match the expected method and path and inspect +the request body when it is part of the contract. When a path contains a dynamic +value, constrain the matcher to the expected path and value format rather than +using a catch-all expression. Use `.times()` when the number of requests matters. +Avoid `.persist()` unless the behaviour genuinely makes an open-ended number of +calls; persistent interceptors can hide unexpected requests. Use +`nock.cleanAll()` to remove direct Nock interceptors after the test, or +`mockManager.restore()` when resetting framework-managed state. Do not use +`nock.restore()` for cleanup: it disables interception and requires +`nock.activate()` before Nock can intercept another request. Use Nock's pending +mocks to diagnose an expectation that was never met. + ## Snapshots Request agents provide `matchBodySnapshot()` and `matchHeaderSnapshot()`. Use