From 98cda2561fecf3ef79deef372bc9e6c9e1150eb4 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 7 Dec 2024 02:46:14 -0800 Subject: [PATCH 1/8] feat: docs laying out the vision for holodeck --- packages/holodeck/docs/holo-programs.md | 306 ++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 packages/holodeck/docs/holo-programs.md diff --git a/packages/holodeck/docs/holo-programs.md b/packages/holodeck/docs/holo-programs.md new file mode 100644 index 0000000000..0c7a5ebe6d --- /dev/null +++ b/packages/holodeck/docs/holo-programs.md @@ -0,0 +1,306 @@ +# HoloPrograms + +
+ +**HoloPrograms** are sets of simulated API interactions that can be used to quickly +set the scene for a test. + +
+ +**Table of Contents** + +- Using HoloPrograms + - [Using a HoloProgram](#using-a-holoprogram) + - [Adjusting a HoloProgram from Within a Test](#adjusting-a-holoprogram-from-within-a-test) + - [HoloProgam Replay](#holoprogram-replay) +- Creating HoloPrograms + - [Creating a HoloProgram](#creating-a-holoprogram) + - [Route Handlers](#1-shared-route-handlers) + - [Seed Data](#2-seed-data) + - [Easy Mode for Building JSON for Seeds](#easy-mode-for-building-up-json-for-seeds) + - [Defining HoloProgram Behaviors](#3-holoprogram-specific-behaviors) + - [Available Behaviors](#available-behaviors) + +
+ +--- + +
+ +### Using a HoloProgram + +A test declares upfront what HoloProgram it is using. + +```ts +import { module, test } from 'qunit'; +import { startProgram, POST } from '@warp-drive/holodeck'; + +module('First Contact', function() { + test('borg are vulnerable to holographic bullets', async function(assert) { + await startProgram(this, 'the-big-goodbye_chapter-13'); + + // now all requests made in this test will be resolved using the program + // defined as 'the-big-goodbye_chapter-13' + + }); +}); +``` + +
+ +### Adjusting a HoloProgram from Within a Test + +HoloPrograms can be adjusted throughout a test if required. For instance, to add handling for a request that wasn't in the original program, or to provide a different response to the next request. + +```ts +import { module, test } from 'qunit'; +import { startProgram , POST } from '@warp-drive/holodeck'; + +module('First Contact', function() { + test('borg are vulnerable to holographic bullets', async function(assert) { + await startProgram(this, 'the-big-goodbye_chapter-13'); + + // now the next POST request to `/casualty` will respond with this payload + // note: this will cause this particular request to NOT update any HoloProgram + // state as it will no longer be handled by the Program + await POST(this, '/casualty', () => ({ + data: { + id: '3', + type: 'casualty', + attributes: { + species: 'human', + affiliation: 'borg', + name: 'Ensign Lynch', + }, + }, + }); + + }); +}); +``` + +To update the state of a HoloProgram from within the test instead, we can use `updateProgram` + +```ts +import { module, test } from 'qunit'; +import { startProgram, updateProgram } from '@warp-drive/holodeck'; + +module('First Contact', function() { + test('borg are vulnerable to holographic bullets', async function(assert) { + await startProgram(this, 'The Big Boodbye | Chapter 13'); + + // the payload provided here will be upserted directly into the program + // cache, and thus should match the cache format in use. + // updates to a program from within a test will not affect any other tests + await updateProgram(this, () => ({ + data: { + id: '3', + type: 'casualty', + attributes: { + species: 'human', + affiliation: 'borg', + name: 'Ensign Lynch', + }, + }, + }); + + // any requests from here out that return `casualty:3` + // will have the updated data + + }); +}); +``` + +
+ +### HoloProgram Replay + +HoloPrograms record all requests for replay the same as any other mocked request, +thus the program does not activate in replay mode. + +
+ +--- + +
+ +## Creating a HoloProgram + +Every HoloProgram consists of three things: route handlers, a seed, and preset behaviors. + +
+ +### 1. Shared Route Handlers + +In holodeck, all HoloPrograms utilize the same underlying route handlers. This encourages writing realistic handlers which in turn makes authoring new tests faster and easier. + +Route Handlers are responsible for parsing a request and providing a response. This typically takes the form of querying (and updating) the HoloProgram's store to match the request's intent. + +Note: any updates made to the store affect future requests within the same test, making it easy to generate realistic API scenarios. + +```ts +import { Router } from '@warp-drive/holodeck'; + +// The holodeck router encourages lazy-evaluation as a pattern +// in order to ensure the server boots and begins responding to +// request as quickly as possible +// +// the router map is only generated if holodeck needs to record +export default new Router((r) => { + r.GET('/officers', async () => { + // the cost of importing and parsing the handler code is only paid if + // a matching request is made. + return (await import('./handlers/officers')).GET; + }) +}); +``` + +
+ +### 2. Seed Data + +While all tests share the same route handlers, each HoloProgram begins from a unique store state. + +The store (and its starting state) are encapsulated to the test context and will never leak between tests, even when tests are recording concurrently. + +The starting seed data should be an array of json resources in the configured cache format, which can be generated via any mechanism desired. + +```ts +import { createProgram } from '@warp-drive/holodeck'; +import { fnThatGeneratesJson } from './my-seed'; + +await createProgram({ + name: 'The Big Goodbye | Chapter 13', + seed: fnThatGeneratesJson, +}); +``` + +In keeping with the encouraged pattern of lazy evaluation, the seed function only executes if the program needs to be booted for a test in record mode. + +
+ +### Easy Mode for Building up JSON for Seeds + +Don't know or understand the cache format? No sweat! + +We can use a store instance to generate the data using the record types and API's we are familiar with from our app, and then serialize this to a seed. + +In general, this allows us to write fairly composable functions to build up our seed quickly. + +For instance: + +```ts +import { serializeCache } from '@warp-drive/holodeck'; +import Store from 'my-enterprise/services/store'; +import type { Officer, Starship } from 'my-enterprise/schema-types'; + +function generateOfficers(store: Store) { + const Picard = store.createRecord('officer', { + id: '1', + name: 'Jean-Luc Picard', + rank: 'Captain' + }); + + const Riker = store.createRecord('officer', { + id: '2', + name: 'William Thomas Riker', + rank: 'First-Officer', + bestFriend: Picard; + }); + + return [Picard, Riker]; +} + +function generateStarship(store: Store, crew: Officer[]) { + const Enterprise = store.createRecord('starship', { + id: 'NCC-1701-D' + name: 'Enterprise', + crew, + }); + + return Enterprise; +} + +export function generateSeed() { + const store = new Store(); + + const officers = generateOfficers(store); + generateStarship(store, officers); + + return serializeCache(store); +} +``` + +A key feature to be aware of is that because all resources MUST have a primaryKey value, `serializeCache` will assign a uuid-v4 as the primaryKey value for any record you have not assigned one to. + +
+ +### 3. HoloProgram Specific Behaviors + +Most HoloPrograms will only ever require a seed to go along with the defined route handlers. But sometimes you may want a holoprogram to simulate externalities. + +Externalities are things like "the API state updated in between the time a user made their last request and their next one" or "this endpoint should have a delay or timeout". + +To handle these sorts of scenarios, HoloPrograms can augment the defined handlers for a specific route: + +```ts +import { createProgram } from '@warp-drive/holodeck'; +import { generateSeed } from './my-seed'; + +await createProgram({ + name: 'The Big Goodbye | Chapter 13', + seed: generateSeed, + behaviors: (r) => { + // passing an object as the second param will apply the adjustment to the route + // on every request + r.GET('/starships', { delay: 50 }); + + // when we pass an array of objects, each object is an adjustment + // for a single request. + // + // The first request to /officers will have a 20ms delay + // The second request to /officers will have a 100ms delay + // The third request to /officers will have no delay + r.GET('/officers', [{ delay: 20 }, { delay: 100 }]); + } +}) +``` + +
+ +### Available Behaviors + +The following behaviors are available: + +```ts +type Adjustment = { + // milliseconds to wait before either invoking the registered + // handler or responding with an error augmentation + requestDelay?: number; + + // milliseconds to wait after invoking the registered handler + // or preparing an error augmentation before sending the response + // back to the client + responseDelay?: number; + + // an error to respond with instead of the handler's usual behavior + // see also the statusCode utils + // the usual handler WILL NOT run + error?: { + status: number; // >= 400; + statusText?: string; // will be autopopulated if not provided based on statusCode + body?: string; + headers?: Headers | Record; + } + + // update store state only after this request + // has completed sending its response + after?: (request: Request, store: Store) => {} +} +``` + +
+ +--- + +
From 17243e788b889330c08b80993232c32ad06bb325 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 7 Dec 2024 16:10:45 -0800 Subject: [PATCH 2/8] safety protocols and vcr mode --- packages/holodeck/docs/holo-programs.md | 92 +++++++++++++++++++++++++ packages/holodeck/docs/vcr-style.md | 37 ++++++++++ packages/holodeck/server/index.js | 3 + 3 files changed, 132 insertions(+) create mode 100644 packages/holodeck/docs/vcr-style.md diff --git a/packages/holodeck/docs/holo-programs.md b/packages/holodeck/docs/holo-programs.md index 0c7a5ebe6d..cbc7b5ffae 100644 --- a/packages/holodeck/docs/holo-programs.md +++ b/packages/holodeck/docs/holo-programs.md @@ -18,6 +18,7 @@ set the scene for a test. - [Route Handlers](#1-shared-route-handlers) - [Seed Data](#2-seed-data) - [Easy Mode for Building JSON for Seeds](#easy-mode-for-building-up-json-for-seeds) + - [Auto-Generating HoloPrograms](#auto-generating-holoprograms) - [Defining HoloProgram Behaviors](#3-holoprogram-specific-behaviors) - [Available Behaviors](#available-behaviors) @@ -155,6 +156,22 @@ export default new Router((r) => { }); ``` +The handler type is: + +```ts +interface RouteHandler { + request(context: Context, request: Request): Response | Value; + + protocols?(v: HolodeckValibot): Record; + + requestProtocol?(request: Request): keyof ReturnType; + responseProtocol?(response: Response): keyof ReturnType; +} +``` + +See also [Safety Protocols](#safety-protocols) + +
### 2. Seed Data @@ -268,6 +285,12 @@ await createProgram({
+### Auto-Generating HoloPrograms + +HoloPrograms can be programatically created! To learn how read about [VCR Style Testing](./vcr-style.md). + +
+ ### Available Behaviors The following behaviors are available: @@ -296,6 +319,42 @@ type Adjustment = { // update store state only after this request // has completed sending its response after?: (request: Request, store: Store) => {} + + // CAUTION: this behavior is only utilized if a + // route handler chooses to use it! + // + // When creating new records it can be useful + // to explicitly declare the desired primaryKey + // value to use + // + // This ID can be accessed by the handler calling + // `.desiredId()`. + // + // For transactional saves (multiple new records in + // a single request) this should be the id for a + // primary new record if present, while any other IDs + // for new records should be contained in a `patch`. + id?: string | number; + + // CAUTION: this behavior is only utilized if a + // route handler chooses to use it! + // + // When creating or updating records, sometimes + // additional fields need to be created or updated. + // + // This behavior stores JSON state that should be + // applied to the store to enable a mutation to + // adequately mirror the behavior of the real API. + // + // The format of this patch is an array of resource + // data that could (if needed) be directly upsert to + // the cache like a seed. + // + // The patch can be accessed by the route handler via + // `.desiredPatch()`. It can be applied either + // manually in the handler or by the handler calling + // `.commitDesiredPatch()` or not at all. + patch?: Value[]; } ``` @@ -304,3 +363,36 @@ type Adjustment = { ---
+ + +## Safety Protocols + +An route handler can declare Safety Protocols that are used to +ensure that both inbound requests and outbound responses match expectations of the API. + +Safety Protocols are written using [Valibot](https://valibot.dev/) a fast, typed schema-validation library. + +### Adding a Safety Protocol + +```ts + +``` + +### Filtering Sensitive Data + +Safety Protocols enable filtering sensitive data from real API responses by utilizing [Transforms](https://valibot.dev/api/transform/). + +While we use Valibot under the hood, Transforms defined on safety protocols are only active when processing the response from a real API request. In all other scenarios the transform passes through the original value. + +This allows the same safety protocol to be used to validate and sanitize the shape of the real API response as is used to validate the shape of the mock API response. + +### Dynamic Safety Protocols + +Some endpoints adjust their behavior based on a request body or header. Protocols can be dynamically swapped. + + +
+ +--- + +
diff --git a/packages/holodeck/docs/vcr-style.md b/packages/holodeck/docs/vcr-style.md new file mode 100644 index 0000000000..36a992d4a7 --- /dev/null +++ b/packages/holodeck/docs/vcr-style.md @@ -0,0 +1,37 @@ +# VCR Style + +VCR refers to a style of request mocking named for a common [20th century technology](https://en.wikipedia.org/wiki/Videocassette_recorder) that was popularized by the [Rails VCR gem](https://github.com/vcr/vcr) and later by [Polly.js]( +https://netflix.github.io/pollyjs/#/README) and [Postman](https://learning.postman.com/docs/designing-and-developing-your-api/mocking-data/setting-up-mock/). + +The basic premise of VCR tests is that during development real network requests made against an API are recorded, then replayed later whenever the test suite is run. + +Holodeck supports dynamically creating and updating [HoloPrograms](./holo-programs.md) from requests made against your real API in two modes: + +- [Static Relay](#static-relay) in which requests are recorded and replayed exactly as initially seen (*caveat* see [Safety Protocols](./holo-programs.md#safety-protocols)) +- [Dynamic Relay](#dynamic-relay) in which some-or-all requests made during a test or app session are intercepted by a temporary HoloProgram and used to update its associated Store and generate a new HoloProgram. + +## Static Relay + +In Static relay, Holodeck re-issues the requests it receives against your real API. Responses are passed through a [Safety Protocol](./holo-programs.md#safety-protocols) before being cached for replay. + +No HoloProgram is generated and no other response alteration is possible. When a test needs its mock requests updated, the requests should be recorded against the real-api again. + +Tests written using the static relay are potentially brittle if care is not taken to write the test in a way that does not expect specific values to be returned from the API. + +If testing specific values is a requirement, a consistent dataset should be used for the API being recorded against. In limited simple scenarios a [Safety Protocol](./holo-programs.md#safety-protocols) may be used to ensure specific values remain consistent across recordings, but this should only be utilized for primitive values and not to adjust data such as relationships or resource identity. + +## Dynamic Relay + +In Dynamic relay, Holodeck re-issues the requests it receives against your real API. Responses are passed through a [Safety Protocol](./holo-programs.md#safety-protocols) before being delivered to an associated Store instance scoped to the current context. + +This store is what will be serialized as the seed for a new HoloProgram. Responses are generated from this store using the configured route handlers. + +Mutations (requests which update state) can also be recorded, though you may find it more pragmatic to stop recording at the first mutation. + +By default, requests using the HTTP methods PUT, PATCH, DELETE and POST (when no `Http-Method-Override=QUERY` header is present) are treated as mutations. This is configurable via the `isMutationRequest` hook in your holodeck config. + +The first mutation encountered results in the store being serialized as the seed right away at that point before the mutation is applied. + +Holodeck then issues the request against the real API. Any delta to the store after the real API responds is then serialized and added to the program as a [patch behavior](./holo-programs.md#available-behaviors). + +If the result is the creation of a single new record in the store, its id will be added to the program as an [id behavior](./holo-programs.md#available-behaviors). If the result is multiple new records, only the patch behavior is added. diff --git a/packages/holodeck/server/index.js b/packages/holodeck/server/index.js index 69c2ffb9cc..65eec6eac8 100644 --- a/packages/holodeck/server/index.js +++ b/packages/holodeck/server/index.js @@ -12,6 +12,9 @@ import zlib from 'node:zlib'; import { homedir } from 'os'; import path from 'path'; +// TODO store blobs in sqlite instead of filesystem? +// TODO use headers instead of query params for test ID and request number + /** @type {import('bun-types')} */ const isBun = typeof Bun !== 'undefined'; const DEBUG = process.env.DEBUG?.includes('holodeck') || process.env.DEBUG === '*'; From 23b7b18058a644f52a4cb21c1def0a997ea0cd79 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sat, 7 Dec 2024 16:12:22 -0800 Subject: [PATCH 3/8] add links --- packages/holodeck/docs/holo-programs.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/holodeck/docs/holo-programs.md b/packages/holodeck/docs/holo-programs.md index cbc7b5ffae..69bb877495 100644 --- a/packages/holodeck/docs/holo-programs.md +++ b/packages/holodeck/docs/holo-programs.md @@ -21,6 +21,10 @@ set the scene for a test. - [Auto-Generating HoloPrograms](#auto-generating-holoprograms) - [Defining HoloProgram Behaviors](#3-holoprogram-specific-behaviors) - [Available Behaviors](#available-behaviors) +- Safety Protocols + - [About Safety Protocols](#safety-protocols) + - [Filtering Sensitive Data](#filtering-sensitive-data) + - [Dynamic Safety Protocls](#dynamic-safety-protocols)
From 8a167aaed63f4c303cea30577f331b8baf842149 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 8 Dec 2024 01:13:56 -0800 Subject: [PATCH 4/8] refactor holodeck to prepare to API expansion --- packages/holodeck/client/env.ts | 35 ++++++++ packages/holodeck/client/handler.ts | 42 +++++++++ packages/holodeck/client/index.ts | 4 + .../{src/mock.ts => client/macros.ts} | 3 +- packages/holodeck/client/mock.ts | 23 +++++ packages/holodeck/client/tsconfig.json | 54 ++++++++++++ packages/holodeck/eslint.config.mjs | 3 +- packages/holodeck/package.json | 31 ++++--- .../holodeck/server/{index.js => index.ts} | 59 +++++++++---- packages/holodeck/server/tsconfig.json | 43 ++++++++++ packages/holodeck/src/index.ts | 86 ------------------- packages/holodeck/tsconfig.json | 55 +----------- packages/holodeck/vite.config.mjs | 3 +- pnpm-lock.yaml | 10 +++ tests/ember-data__json-api/diagnostic.js | 2 +- tests/ember-data__request/diagnostic.js | 2 +- .../tests/integration/fetch-handler-test.ts | 3 +- tests/warp-drive__ember/diagnostic.js | 2 +- .../integration/get-request-state-test.gts | 3 +- .../request-component-invalidation-test.gts | 3 +- .../request-component-subscription-test.gts | 3 +- .../integration/request-component-test.gts | 3 +- tests/warp-drive__experiments/diagnostic.js | 2 +- .../integration/data-worker/basic-test.ts | 3 +- .../data-worker/serialization-test.ts | 3 +- 25 files changed, 286 insertions(+), 194 deletions(-) create mode 100644 packages/holodeck/client/env.ts create mode 100644 packages/holodeck/client/handler.ts create mode 100644 packages/holodeck/client/index.ts rename packages/holodeck/{src/mock.ts => client/macros.ts} (95%) create mode 100644 packages/holodeck/client/mock.ts create mode 100644 packages/holodeck/client/tsconfig.json rename packages/holodeck/server/{index.js => index.ts} (89%) create mode 100644 packages/holodeck/server/tsconfig.json delete mode 100644 packages/holodeck/src/index.ts diff --git a/packages/holodeck/client/env.ts b/packages/holodeck/client/env.ts new file mode 100644 index 0000000000..adfaffa0ba --- /dev/null +++ b/packages/holodeck/client/env.ts @@ -0,0 +1,35 @@ +let IS_RECORDING = false; +export function setIsRecording(value: boolean) { + IS_RECORDING = Boolean(value); +} +export function getIsRecording() { + return IS_RECORDING; +} + +export type TestInfo = { id: string; request: number; mock: number }; +const TEST_IDS = new WeakMap(); + +let HOST = 'https://localhost:1135/'; +export function setConfig({ host }: { host: string }) { + HOST = host.endsWith('/') ? host : `${host}/`; +} + +export function getConfig(): { host: string } { + return { host: HOST }; +} + +export function setTestId(context: object, str: string | null) { + if (str && TEST_IDS.has(context)) { + throw new Error(`MockServerHandler is already configured with a testId.`); + } + if (str) { + TEST_IDS.set(context, { id: str, request: 0, mock: 0 }); + } else { + TEST_IDS.delete(context); + } +} + +export function getTestInfo(context: object): TestInfo | null { + const test = TEST_IDS.get(context); + return test ?? null; +} diff --git a/packages/holodeck/client/handler.ts b/packages/holodeck/client/handler.ts new file mode 100644 index 0000000000..2a3ab4b9e1 --- /dev/null +++ b/packages/holodeck/client/handler.ts @@ -0,0 +1,42 @@ +import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request'; + +import { getTestInfo } from './env'; + +export class MockServerHandler implements Handler { + declare owner: object; + constructor(owner: object) { + this.owner = owner; + } + + async request(context: RequestContext, next: NextFn): Promise> { + const test = getTestInfo(this.owner); + if (!test) { + throw new Error( + `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test` + ); + } + + const request: RequestInfo = Object.assign({}, context.request); + const isRecording = request.url!.endsWith('/__record'); + const firstChar = request.url!.includes('?') ? '&' : '?'; + const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${ + isRecording ? test.mock++ : test.request++ + }`; + request.url = request.url + queryForTest; + + request.mode = 'cors'; + request.credentials = 'omit'; + request.referrerPolicy = ''; + + try { + const future = next(request); + context.setStream(future.getStream()); + return await future; + } catch (e) { + if (e instanceof Error && !(e instanceof DOMException)) { + e.message = e.message.replace(queryForTest, ''); + } + throw e; + } + } +} diff --git a/packages/holodeck/client/index.ts b/packages/holodeck/client/index.ts new file mode 100644 index 0000000000..79ab456c01 --- /dev/null +++ b/packages/holodeck/client/index.ts @@ -0,0 +1,4 @@ +export { GET, PATCH, POST, PUT, DELETE, QUERY } from './macros'; +export { setTestId } from './env'; +export { mock } from './mock'; +export { MockServerHandler } from './handler'; diff --git a/packages/holodeck/src/mock.ts b/packages/holodeck/client/macros.ts similarity index 95% rename from packages/holodeck/src/mock.ts rename to packages/holodeck/client/macros.ts index 1d2697ef23..28a5c7c96e 100644 --- a/packages/holodeck/src/mock.ts +++ b/packages/holodeck/client/macros.ts @@ -1,4 +1,5 @@ -import { getIsRecording, mock } from '.'; +import { getIsRecording } from './env'; +import { mock } from './mock'; export interface Scaffold { status: number; diff --git a/packages/holodeck/client/mock.ts b/packages/holodeck/client/mock.ts new file mode 100644 index 0000000000..8eeef5dcc3 --- /dev/null +++ b/packages/holodeck/client/mock.ts @@ -0,0 +1,23 @@ +import { getConfig, getIsRecording, getTestInfo } from './env'; +import type { ScaffoldGenerator } from './macros'; + +export async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) { + const config = getConfig(); + + const test = getTestInfo(owner); + if (!test) { + throw new Error(`Cannot call "mock" before configuring a testId. Use setTestId to set the testId for each test`); + } + const testMockNum = test.mock++; + if (getIsRecording() || isRecording) { + const port = window.location.port ? `:${window.location.port}` : ''; + const url = `${config.host}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`; + await fetch(url, { + method: 'POST', + body: JSON.stringify(generate()), + mode: 'cors', + credentials: 'omit', + referrerPolicy: '', + }); + } +} diff --git a/packages/holodeck/client/tsconfig.json b/packages/holodeck/client/tsconfig.json new file mode 100644 index 0000000000..92e2e43d2c --- /dev/null +++ b/packages/holodeck/client/tsconfig.json @@ -0,0 +1,54 @@ +{ + "include": ["./**/*"], + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + "composite": true, + "incremental": true, + "rootDir": ".", + // Support generation of source maps. Note: you must *also* enable source + // maps in your `ember-cli-babel` config and/or `babel.config.js`. + "declaration": true, + "declarationMap": true, + "declarationDir": "../unstable-preview-types/client", + "inlineSourceMap": true, + "inlineSources": true, + "baseUrl": ".", + "paths": { + "@ember-data/request": ["../../request/unstable-preview-types"], + "@ember-data/request/*": ["../../request/unstable-preview-types/*"], + "@warp-drive/core-types": ["../../core-types/unstable-preview-types"], + "@warp-drive/core-types/*": ["../../core-types/unstable-preview-types/*"] + } + }, + "references": [ + { + "path": "../../request" + }, + { + "path": "../../core-types" + } + ] +} diff --git a/packages/holodeck/eslint.config.mjs b/packages/holodeck/eslint.config.mjs index 3b45156a9d..9fccc77f43 100644 --- a/packages/holodeck/eslint.config.mjs +++ b/packages/holodeck/eslint.config.mjs @@ -10,7 +10,8 @@ export default [ // browser (js/ts) ================ typescript.browser({ - srcDirs: ['src'], + tsconfigRootDir: `${__dirname}/client`, + srcDirs: ['client'], allowedImports: [], }), diff --git a/packages/holodeck/package.json b/packages/holodeck/package.json index 5e0b925080..446ada0b8f 100644 --- a/packages/holodeck/package.json +++ b/packages/holodeck/package.json @@ -41,7 +41,7 @@ }, "scripts": { "check:pkg-types": "tsc --noEmit", - "build:pkg": "vite build;", + "build:pkg": "vite build; bun build --entrypoints ./server/index.ts ./server/ensure-cert.js --target bun --outdir ./dist/server", "prepack": "bun run build:pkg", "sync-hardlinks": "bun run sync-dependencies-meta-injected" }, @@ -59,27 +59,26 @@ "@warp-drive/core-types": "workspace:0.0.0-alpha.106", "@warp-drive/internal-config": "workspace:5.4.0-alpha.120", "pnpm-sync-dependencies-meta-injected": "0.0.14", + "bun-types": "1.1.38", "typescript": "^5.4.5", "vite": "^5.2.11" }, "exports": { ".": { - "node": "./server/index.js", - "bun": "./server/index.js", - "deno": "./server/index.js", - "browser": { - "types": "./unstable-preview-types/index.d.ts", - "default": "./dist/index.js" - }, - "import": { - "types": "./unstable-preview-types/index.d.ts", - "default": "./dist/index.js" - }, - "default": "./server/index.js" + "default": "./dist/client/index.js", + "types": "./unstable-preview-types/client/index.d.ts" }, - "./mock": { - "types": "./unstable-preview-types/mock.d.ts", - "default": "./dist/mock.js" + "./client/*": { + "default": "./dist/client/*.js", + "types": "./unstable-preview-types/client/*.d.ts" + }, + "./server": { + "default": "./dist/server/index.js", + "types": "./unstable-preview-types/server/*.d.ts" + }, + "./server/*": { + "default": "./dist/server/*.js", + "types": "./unstable-preview-types/server/*.d.ts" } }, "dependenciesMeta": { diff --git a/packages/holodeck/server/index.js b/packages/holodeck/server/index.ts similarity index 89% rename from packages/holodeck/server/index.js rename to packages/holodeck/server/index.ts index 65eec6eac8..c825e4c5ca 100644 --- a/packages/holodeck/server/index.js +++ b/packages/holodeck/server/index.ts @@ -1,7 +1,7 @@ /* global Bun */ import { serve } from '@hono/node-server'; import chalk from 'chalk'; -import { Hono } from 'hono'; +import { Context, Hono, MiddlewareHandler } from 'hono'; import { cors } from 'hono/cors'; import { HTTPException } from 'hono/http-exception'; import { logger } from 'hono/logger'; @@ -62,14 +62,14 @@ const BROTLI_OPTIONS = { [zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY, }, }; -function compress(code) { +function compress(code: string) { return zlib.brotliCompressSync(code, BROTLI_OPTIONS); } /** * removes the protocol, host, and port from a url */ -function getNiceUrl(url) { +function getNiceUrl(url: string) { const urlObj = new URL(url); urlObj.searchParams.delete('__xTestId'); urlObj.searchParams.delete('__xTestRequestNumber'); @@ -86,18 +86,31 @@ function getNiceUrl(url) { testRequestNumber: number } */ -function generateFilepath(options) { +function generateFilepath(options: { + body: string | null; + projectRoot: string; + testId: string; + url: string; + method: string; + testRequestNumber: string; +}) { const { body } = options; const bodyHash = body ? crypto.createHash('md5').update(body).digest('hex') : null; const cacheDir = generateFileDir(options); return `${cacheDir}/${bodyHash ? `${bodyHash}-` : 'res'}`; } -function generateFileDir(options) { +function generateFileDir(options: { + projectRoot: string; + testId: string; + url: string; + method: string; + testRequestNumber: string; +}) { const { projectRoot, testId, url, method, testRequestNumber } = options; return `${projectRoot}/.mock-cache/${testId}/${method}-${testRequestNumber}-${url}`; } -function replayRequest(context, cacheKey) { +function replayRequest(context: Context, cacheKey: string) { let meta; try { meta = fs.readFileSync(`${cacheKey}.meta.json`, 'utf-8'); @@ -136,8 +149,8 @@ function replayRequest(context, cacheKey) { return response; } -function createTestHandler(projectRoot) { - const TestHandler = async (context) => { +function createTestHandler(projectRoot: string): MiddlewareHandler { + return async (context) => { try { const { req } = context; @@ -214,7 +227,11 @@ function createTestHandler(projectRoot) { `${cacheKey}.meta.json`, JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2) ); - fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response))); + fs.writeFileSync( + `${cacheKey}.body.br`, + // @ts-expect-error bun seems to break Buffer types + compress(JSON.stringify(response)) + ); context.status(204); return context.body(null); } else { @@ -242,21 +259,16 @@ function createTestHandler(projectRoot) { status: '500', code: 'MOCK_SERVER_ERROR', title: 'Mock Server Error during Request', - detail: e.message, + detail: getErrorMessage(e), }, ], }) ); } }; - - return TestHandler; } -/* -{ port?: number, projectRoot: string } -*/ -export function createServer(options) { +export function createServer(options: { port?: number; projectRoot: string }) { const app = new Hono(); if (DEBUG) { app.use('*', logger()); @@ -279,7 +291,11 @@ export function createServer(options) { serve({ fetch: app.fetch, - createServer: (_, requestListener) => { + // @ts-expect-error, unclear what is wrong with Bun's types here + createServer: ( + _: unknown, + requestListener: (request: http2.Http2ServerRequest, response: http2.Http2ServerResponse) => void + ) => { try { return http2.createSecureServer( { @@ -288,8 +304,9 @@ export function createServer(options) { }, requestListener ); - } catch (e) { - console.log(chalk.yellow(`Failed to create secure server, falling back to http server. Error: ${e.message}`)); + } catch (e: unknown) { + const message = getErrorMessage(e); + console.log(chalk.yellow(`Failed to create secure server, falling back to http server. Error: ${message}`)); return http2.createServer(requestListener); } }, @@ -383,4 +400,8 @@ function main() { } } +function getErrorMessage(e: unknown) { + return e instanceof Error ? e.message : typeof e === 'string' ? e : String(e); +} + main(); diff --git a/packages/holodeck/server/tsconfig.json b/packages/holodeck/server/tsconfig.json new file mode 100644 index 0000000000..7acb79ad9d --- /dev/null +++ b/packages/holodeck/server/tsconfig.json @@ -0,0 +1,43 @@ +{ + "include": ["./**/*"], + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "skipLibCheck": true, + "emitDeclarationOnly": true, + "allowJs": false, + "checkJs": false, + "alwaysStrict": true, + "strict": true, + "pretty": true, + "exactOptionalPropertyTypes": false, + "allowSyntheticDefaultImports": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictBindCallApply": true, + "strictFunctionTypes": true, + "strictPropertyInitialization": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "noEmitOnError": false, + "strictNullChecks": true, + "noErrorTruncation": true, + "preserveConstEnums": false, + "experimentalDecorators": true, + "composite": true, + "incremental": true, + "rootDir": ".", + // Support generation of source maps. Note: you must *also* enable source + // maps in your `ember-cli-babel` config and/or `babel.config.js`. + "declaration": true, + "declarationMap": true, + "declarationDir": "../unstable-preview-types/server", + "inlineSourceMap": true, + "inlineSources": true, + "baseUrl": ".", + "types": ["bun-types"] + } +} diff --git a/packages/holodeck/src/index.ts b/packages/holodeck/src/index.ts deleted file mode 100644 index 98a2008bce..0000000000 --- a/packages/holodeck/src/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocument } from '@ember-data/request'; - -import type { ScaffoldGenerator } from './mock'; - -const TEST_IDS = new WeakMap(); - -let HOST = 'https://localhost:1135/'; -export function setConfig({ host }: { host: string }) { - HOST = host.endsWith('/') ? host : `${host}/`; -} - -export function setTestId(context: object, str: string | null) { - if (str && TEST_IDS.has(context)) { - throw new Error(`MockServerHandler is already configured with a testId.`); - } - if (str) { - TEST_IDS.set(context, { id: str, request: 0, mock: 0 }); - } else { - TEST_IDS.delete(context); - } -} - -let IS_RECORDING = false; -export function setIsRecording(value: boolean) { - IS_RECORDING = Boolean(value); -} -export function getIsRecording() { - return IS_RECORDING; -} - -export class MockServerHandler implements Handler { - declare owner: object; - constructor(owner: object) { - this.owner = owner; - } - async request(context: RequestContext, next: NextFn): Promise> { - const test = TEST_IDS.get(this.owner); - if (!test) { - throw new Error( - `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test` - ); - } - - const request: RequestInfo = Object.assign({}, context.request); - const isRecording = request.url!.endsWith('/__record'); - const firstChar = request.url!.includes('?') ? '&' : '?'; - const queryForTest = `${firstChar}__xTestId=${test.id}&__xTestRequestNumber=${ - isRecording ? test.mock++ : test.request++ - }`; - request.url = request.url + queryForTest; - - request.mode = 'cors'; - request.credentials = 'omit'; - request.referrerPolicy = ''; - - try { - const future = next(request); - context.setStream(future.getStream()); - return await future; - } catch (e) { - if (e instanceof Error && !(e instanceof DOMException)) { - e.message = e.message.replace(queryForTest, ''); - } - throw e; - } - } -} - -export async function mock(owner: object, generate: ScaffoldGenerator, isRecording?: boolean) { - const test = TEST_IDS.get(owner); - if (!test) { - throw new Error(`Cannot call "mock" before configuring a testId. Use setTestId to set the testId for each test`); - } - const testMockNum = test.mock++; - if (getIsRecording() || isRecording) { - const port = window.location.port ? `:${window.location.port}` : ''; - const url = `${HOST}__record?__xTestId=${test.id}&__xTestRequestNumber=${testMockNum}`; - await fetch(url, { - method: 'POST', - body: JSON.stringify(generate()), - mode: 'cors', - credentials: 'omit', - referrerPolicy: '', - }); - } -} diff --git a/packages/holodeck/tsconfig.json b/packages/holodeck/tsconfig.json index 6454af9325..9314b0d6bc 100644 --- a/packages/holodeck/tsconfig.json +++ b/packages/holodeck/tsconfig.json @@ -1,54 +1,5 @@ { - "include": ["src/**/*"], - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "skipLibCheck": true, - "emitDeclarationOnly": true, - "allowJs": false, - "checkJs": false, - "alwaysStrict": true, - "strict": true, - "pretty": true, - "exactOptionalPropertyTypes": false, - "allowSyntheticDefaultImports": true, - "noImplicitAny": true, - "noImplicitThis": true, - "strictBindCallApply": true, - "strictFunctionTypes": true, - "strictPropertyInitialization": true, - "allowUnreachableCode": false, - "allowUnusedLabels": false, - "noEmitOnError": false, - "strictNullChecks": true, - "noErrorTruncation": true, - "preserveConstEnums": false, - "experimentalDecorators": true, - "composite": true, - "incremental": true, - "rootDir": "src", - // Support generation of source maps. Note: you must *also* enable source - // maps in your `ember-cli-babel` config and/or `babel.config.js`. - "declaration": true, - "declarationMap": true, - "declarationDir": "unstable-preview-types", - "inlineSourceMap": true, - "inlineSources": true, - "baseUrl": ".", - "paths": { - "@ember-data/request": ["../request/unstable-preview-types"], - "@ember-data/request/*": ["../request/unstable-preview-types/*"], - "@warp-drive/core-types": ["../core-types/unstable-preview-types"], - "@warp-drive/core-types/*": ["../core-types/unstable-preview-types/*"] - } - }, - "references": [ - { - "path": "../request" - }, - { - "path": "../core-types" - } - ] + "files": [], + "include": [], + "references": [{ "path": "./server" }, { "path": "./client" }] } diff --git a/packages/holodeck/vite.config.mjs b/packages/holodeck/vite.config.mjs index 56e2bc98d1..4fd850faf5 100644 --- a/packages/holodeck/vite.config.mjs +++ b/packages/holodeck/vite.config.mjs @@ -1,10 +1,11 @@ import { createConfig } from '@warp-drive/internal-config/vite/config.js'; export const externals = []; -export const entryPoints = ['./src/index.ts', './src/mock.ts']; +export const entryPoints = ['./client/index.ts']; export default createConfig( { + srcDir: './client', entryPoints, externals, fixModule: false, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 460155a916..0355f0d1a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1118,6 +1118,9 @@ importers: '@warp-drive/internal-config': specifier: workspace:5.4.0-alpha.120 version: link:../../config + bun-types: + specifier: 1.1.38 + version: 1.1.38 pnpm-sync-dependencies-meta-injected: specifier: 0.0.14 version: 0.0.14 @@ -12776,6 +12779,13 @@ packages: '@types/ws': 8.5.10 dev: true + /bun-types@1.1.38: + resolution: {integrity: sha512-iglB2t9z1Hc6DIuwwscwWj/csx22QlCZ96QbcqQfiy1wmuZ38srQLI/fDVkFHAo2+KL7aJZGVWF+nAWrR6Njig==} + dependencies: + '@types/node': 20.12.12 + '@types/ws': 8.5.10 + dev: true + /bytes@1.0.0: resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==} diff --git a/tests/ember-data__json-api/diagnostic.js b/tests/ember-data__json-api/diagnostic.js index 28c6a933ba..301a36711d 100644 --- a/tests/ember-data__json-api/diagnostic.js +++ b/tests/ember-data__json-api/diagnostic.js @@ -1,5 +1,5 @@ import launch from '@warp-drive/diagnostic/server/default-setup.js'; -import holodeck from '@warp-drive/holodeck'; +import holodeck from '@warp-drive/holodeck/server'; await launch({ async setup(options) { diff --git a/tests/ember-data__request/diagnostic.js b/tests/ember-data__request/diagnostic.js index 3d888b65b6..4e456f2d02 100644 --- a/tests/ember-data__request/diagnostic.js +++ b/tests/ember-data__request/diagnostic.js @@ -1,5 +1,5 @@ import launch from '@warp-drive/diagnostic/server/default-setup.js'; -import holodeck from '@warp-drive/holodeck'; +import holodeck from '@warp-drive/holodeck/server'; await launch({ async setup(info) { diff --git a/tests/ember-data__request/tests/integration/fetch-handler-test.ts b/tests/ember-data__request/tests/integration/fetch-handler-test.ts index cea431bdc6..1c7ddd9201 100644 --- a/tests/ember-data__request/tests/integration/fetch-handler-test.ts +++ b/tests/ember-data__request/tests/integration/fetch-handler-test.ts @@ -2,8 +2,7 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { buildBaseURL } from '@ember-data/request-utils'; import { module, test } from '@warp-drive/diagnostic'; -import { mock, MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; const RECORD = false; diff --git a/tests/warp-drive__ember/diagnostic.js b/tests/warp-drive__ember/diagnostic.js index 28c6a933ba..301a36711d 100644 --- a/tests/warp-drive__ember/diagnostic.js +++ b/tests/warp-drive__ember/diagnostic.js @@ -1,5 +1,5 @@ import launch from '@warp-drive/diagnostic/server/default-setup.js'; -import holodeck from '@warp-drive/holodeck'; +import holodeck from '@warp-drive/holodeck/server'; await launch({ async setup(options) { diff --git a/tests/warp-drive__ember/tests/integration/get-request-state-test.gts b/tests/warp-drive__ember/tests/integration/get-request-state-test.gts index 2ec05bbb3a..d5e58d92a5 100644 --- a/tests/warp-drive__ember/tests/integration/get-request-state-test.gts +++ b/tests/warp-drive__ember/tests/integration/get-request-state-test.gts @@ -7,8 +7,7 @@ import { buildBaseURL } from '@ember-data/request-utils'; import type { RenderingTestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { getRequestState } from '@warp-drive/ember'; -import { mock, MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; type RequestState = ReturnType>; type UserResource = { diff --git a/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts b/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts index 7f54a40b96..892e5c3afe 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts @@ -14,8 +14,7 @@ import type { Diagnostic } from '@warp-drive/diagnostic/-types'; import type { RenderingTestContext, TestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { Request } from '@warp-drive/ember'; -import { MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, MockServerHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; diff --git a/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts b/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts index 52549bc1ff..a592c14d3b 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts @@ -14,8 +14,7 @@ import type { Diagnostic } from '@warp-drive/diagnostic/-types'; import type { RenderingTestContext, TestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { Request } from '@warp-drive/ember'; -import { MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, MockServerHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; diff --git a/tests/warp-drive__ember/tests/integration/request-component-test.gts b/tests/warp-drive__ember/tests/integration/request-component-test.gts index fc996b1485..d749b55f9e 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-test.gts @@ -14,8 +14,7 @@ import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/doc import type { RenderingTestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { getRequestState, Request } from '@warp-drive/ember'; -import { mock, MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; // our tests use a rendering test context and add manager to it diff --git a/tests/warp-drive__experiments/diagnostic.js b/tests/warp-drive__experiments/diagnostic.js index 28c6a933ba..301a36711d 100644 --- a/tests/warp-drive__experiments/diagnostic.js +++ b/tests/warp-drive__experiments/diagnostic.js @@ -1,5 +1,5 @@ import launch from '@warp-drive/diagnostic/server/default-setup.js'; -import holodeck from '@warp-drive/holodeck'; +import holodeck from '@warp-drive/holodeck/server'; await launch({ async setup(options) { diff --git a/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts b/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts index c06895de95..70f726a858 100644 --- a/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts +++ b/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts @@ -6,8 +6,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { module, test } from '@warp-drive/diagnostic'; import { WorkerFetch } from '@warp-drive/experiments/worker-fetch'; -import { MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, MockServerHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService } from '@warp-drive/schema-record/schema'; diff --git a/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts b/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts index dc1b547373..bd26be2b94 100644 --- a/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts +++ b/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts @@ -6,8 +6,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { module, test } from '@warp-drive/diagnostic'; import { WorkerFetch } from '@warp-drive/experiments/worker-fetch'; -import { MockServerHandler } from '@warp-drive/holodeck'; -import { GET } from '@warp-drive/holodeck/mock'; +import { GET, MockServerHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService } from '@warp-drive/schema-record/schema'; From 98e5e4d510d4e2de131cb19a50fa0d0039f7152b Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 8 Dec 2024 01:19:24 -0800 Subject: [PATCH 5/8] cleanup modules --- packages/holodeck/package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/holodeck/package.json b/packages/holodeck/package.json index 446ada0b8f..4ab65b8d12 100644 --- a/packages/holodeck/package.json +++ b/packages/holodeck/package.json @@ -65,20 +65,20 @@ }, "exports": { ".": { - "default": "./dist/client/index.js", - "types": "./unstable-preview-types/client/index.d.ts" + "types": "./unstable-preview-types/client/index.d.ts", + "default": "./dist/client/index.js" }, "./client/*": { - "default": "./dist/client/*.js", - "types": "./unstable-preview-types/client/*.d.ts" + "types": "./unstable-preview-types/client/*.d.ts", + "default": "./dist/client/*.js" }, "./server": { - "default": "./dist/server/index.js", - "types": "./unstable-preview-types/server/*.d.ts" + "types": "./unstable-preview-types/server/index.d.ts", + "default": "./dist/server/index.js" }, "./server/*": { - "default": "./dist/server/*.js", - "types": "./unstable-preview-types/server/*.d.ts" + "types": "./unstable-preview-types/server/*.d.ts", + "default": "./dist/server/*.js" } }, "dependenciesMeta": { From 0c2697f6493aab7dea5a171e885296a52fe94f12 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 8 Dec 2024 01:21:15 -0800 Subject: [PATCH 6/8] ignore it? --- packages/holodeck/server/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/holodeck/server/index.ts b/packages/holodeck/server/index.ts index c825e4c5ca..33a386cc61 100644 --- a/packages/holodeck/server/index.ts +++ b/packages/holodeck/server/index.ts @@ -227,11 +227,7 @@ function createTestHandler(projectRoot: string): MiddlewareHandler { `${cacheKey}.meta.json`, JSON.stringify({ url, status, statusText, headers, method, requestBody: body }, null, 2) ); - fs.writeFileSync( - `${cacheKey}.body.br`, - // @ts-expect-error bun seems to break Buffer types - compress(JSON.stringify(response)) - ); + fs.writeFileSync(`${cacheKey}.body.br`, compress(JSON.stringify(response))); context.status(204); return context.body(null); } else { From 99f289b891ffedf3a91fa7fc599ad5f64d5a7f0c Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 8 Dec 2024 14:16:13 -0800 Subject: [PATCH 7/8] docs and naming cleanup --- packages/holodeck/README.md | 212 +++--------------- packages/holodeck/client/env.ts | 2 +- packages/holodeck/client/handler.ts | 6 +- packages/holodeck/client/index.ts | 2 +- packages/holodeck/docs/basic-concepts.md | 20 ++ packages/holodeck/docs/motivations.md | 21 ++ packages/holodeck/docs/request-integration.md | 37 +++ packages/holodeck/docs/server-setup.md | 73 ++++++ .../docs/test-framework-integration.md | 55 +++++ packages/holodeck/server/index.ts | 4 +- .../integration/cache/error-documents-test.ts | 4 +- .../tests/integration/fetch-handler-test.ts | 8 +- .../integration/get-request-state-test.gts | 4 +- .../request-component-invalidation-test.gts | 4 +- .../request-component-subscription-test.gts | 4 +- .../integration/request-component-test.gts | 6 +- .../integration/data-worker/basic-test.ts | 4 +- .../data-worker/serialization-test.ts | 4 +- 18 files changed, 256 insertions(+), 214 deletions(-) create mode 100644 packages/holodeck/docs/basic-concepts.md create mode 100644 packages/holodeck/docs/motivations.md create mode 100644 packages/holodeck/docs/request-integration.md create mode 100644 packages/holodeck/docs/server-setup.md create mode 100644 packages/holodeck/docs/test-framework-integration.md diff --git a/packages/holodeck/README.md b/packages/holodeck/README.md index 4a8263a486..5591ca373a 100644 --- a/packages/holodeck/README.md +++ b/packages/holodeck/README.md @@ -36,10 +36,31 @@ - 🔥 Blazing Fast Tests - record your tests when you change them - replays from cache until you change them again + - the cache is managed by git, so changing branches works seamlessly as does skipping unneeded work in CI or while rebasing - zero-work: setup work is skipped when in replay mode -## Installation +
+ +## Documentation + +- [Motivations](./docs/motivations.md) +- [Server Setup](./docs/server-setup.md) +- [Client Setup](./docs/request-integration.md) +- [Test Framework Integration](./docs/test-framework-integration.md) +- [HoloPrograms](./docs/holo-programs.md) + - [Route Handlers](./docs/holo-programs.md#1-shared-route-handlers) + - [Seed Data](./docs/holo-programs.md#2-seed-data) + - [Behaviors](./docs/holo-programs.md#3-holoprogram-specific-behaviors) +- [Safety Protocols](./docs/holo-programs.md#safety-protocols) +- [VCR Style Tests](./docs/vcr-style.md) + +
+--- + +
+ +## Installation ```json pnpm install @warp-drive/holodeck @@ -53,192 +74,9 @@ pnpm install @warp-drive/holodeck - ![NPM LTS Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/lts?label=%40lts&color=0096FF) - ![NPM LTS 4.12 Version](https://img.shields.io/npm/v/%40warp-drive/holodeck/lts-4-12?label=%40lts-4-12&color=bbbbbb) - - -## Usage -#### Mocking from Within a Test - -```ts -import { GET } from '@warp-drive/holodeck/mock'; - -await GET(context, 'users/1', () => ({ - data: { - id: '1', - type: 'user', - attributes: { - name: 'Chris Thoburn', - }, - }, - -// set RECORD to false or remove -// the options hash entirely once the request -// has been recorded -}), { RECORD: true }); -``` - -## Motivations - -Comprehensive DX around data management should extend to testing. - -### ✨ Amazing Developer Experience - -EmberData already understands your data schemas. Building a mocking utility with tight integration into your data usage patterns could bring enormous DX and test suite performance benefits. - -Building a real mock server instead of intercepting requests in the browser or via ServiceWorker gives us out-of-the-box DX, better tunability, and greater ability to optimize test suite performance. Speed is the ultimate DX. - -### 🔥 Blazing Fast Tests - -We've noticed test suites spending an enormous amount of time creating and tearing down mock state in between tests. To combat this, we want to provide -an approach built over `http/3` (`http/2` for now) utilizing aggressive caching -and `brotli` minification in a way that can be replayed over and over again. - -Basically, pay the cost when you write the test. Forever after skip the cost until you need to edit the test again. - -## Setup - -### Use with WarpDrive - -First, you will need to add the holodeck handler to the request manager chain prior to `Fetch` (or any equivalent handler that proceeds to network). - -For instance: - -```ts -import RequestManager from '@ember-data/request'; -import Fetch from '@ember-data/request/fetch'; -import { MockServerHandler } from '@warp-drive/holodeck'; - -const manager = new RequestManager(); -manager.use([new MockServerHandler(testContext), Fetch]); -``` - -From within a test this might look like: - -```ts -import RequestManager from '@ember-data/request'; -import Fetch from '@ember-data/request/fetch'; -import { MockServerHandler } from '@warp-drive/holodeck'; -import { module, test } from 'qunit'; - -module('my module', function() { - test('my test', async function() { - const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); - }); -}); -``` - -Next, you will need to configure holodeck to understand your tests contexts. For qunit and diagnostic -in a project using Ember this is typically done in `tests/test-helper.js` - -#### With Diagnostic - -```ts -import { setupGlobalHooks } from '@warp-drive/diagnostic'; -import { setConfig, setTestId } from '@warp-drive/holodeck'; - -// if not proxying the port / set port to the correct value here -const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; - -setConfig({ host: MockHost }); - -setupGlobalHooks((hooks) => { - hooks.beforeEach(function (assert) { - setTestId(this, assert.test.testId); - }); - hooks.afterEach(function () { - setTestId(this, null); - }); -}); -``` - -#### With QUnit - -```ts -import * as QUnit from 'qunit'; -import { setConfig, setTestId } from '@warp-drive/holodeck'; - -// if not proxying the port / set port to the correct value here -const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; - -setConfig({ host: MockHost }); - -QUnit.hooks.beforeEach(function (assert) { - setTestId(assert.test.testId); -}); -QUnit.hooks.afterEach(function (assert) { - setTestId(null); -}); -``` - -### Testem - -You can integrate holodeck with Testem using testem's [async config capability](https://github.com/testem/testem/blob/master/docs/config_file.md#returning-a-promise-from-testemjs): - -```ts -module.exports = async function () { - const holodeck = (await import('@warp-drive/holodeck')).default; - await holodeck.launchProgram({ - port: 7373, - }); - - process.on('beforeExit', async () => { - await holodeck.endProgram(); - }); - - return { - // ... testem config - }; -}; -``` - -If you need the API mock to run on the same port as the test suite, you can use Testem's [API Proxy](https://github.com/testem/testem/tree/master?tab=readme-ov-file#api-proxy) - -```ts -module.exports = async function () { - const holodeck = (await import('@warp-drive/holodeck')).default; - await holodeck.launchProgram({ - port: 7373, - }); - - process.on('beforeExit', async () => { - await holodeck.endProgram(); - }); - - return { - "proxies": { - "/api": { - // holodeck always runs on https - // the proxy is transparent so this means /api/v1 will route to https://localhost:7373/api/v1 - "target": "https://localhost:7373", - // "onlyContentTypes": ["xml", "json"], - // if test suite is on http, set this to false - // "secure": false, - }, - } - }; -}; -``` - -### Diagnostic - -holodeck can be launched and cleaned up using the lifecycle hooks in the launch config -for diagnostic in `diagnostic.js`: - -```ts -import launch from '@warp-drive/diagnostic/server/default-setup.js'; -import holodeck from '@warp-drive/holodeck'; - -await launch({ - async setup(options) { - await holodeck.launchProgram({ - port: options.port + 1, - }); - }, - async cleanup() { - await holodeck.endProgram(); - }, -}); -``` +
+
+
### ♥️ Credits diff --git a/packages/holodeck/client/env.ts b/packages/holodeck/client/env.ts index adfaffa0ba..c3df17e4e3 100644 --- a/packages/holodeck/client/env.ts +++ b/packages/holodeck/client/env.ts @@ -20,7 +20,7 @@ export function getConfig(): { host: string } { export function setTestId(context: object, str: string | null) { if (str && TEST_IDS.has(context)) { - throw new Error(`MockServerHandler is already configured with a testId.`); + throw new Error(`The Holodeck mock handler is already configured with a testId.`); } if (str) { TEST_IDS.set(context, { id: str, request: 0, mock: 0 }); diff --git a/packages/holodeck/client/handler.ts b/packages/holodeck/client/handler.ts index 2a3ab4b9e1..314aa84f1a 100644 --- a/packages/holodeck/client/handler.ts +++ b/packages/holodeck/client/handler.ts @@ -2,7 +2,7 @@ import type { Handler, NextFn, RequestContext, RequestInfo, StructuredDataDocume import { getTestInfo } from './env'; -export class MockServerHandler implements Handler { +export class HolodeckHandler implements Handler { declare owner: object; constructor(owner: object) { this.owner = owner; @@ -11,9 +11,7 @@ export class MockServerHandler implements Handler { async request(context: RequestContext, next: NextFn): Promise> { const test = getTestInfo(this.owner); if (!test) { - throw new Error( - `MockServerHandler is not configured with a testId. Use setTestId to set the testId for each test` - ); + throw new Error(`HolodeckHandler is not configured with a testId. Use setTestId to set the testId for each test`); } const request: RequestInfo = Object.assign({}, context.request); diff --git a/packages/holodeck/client/index.ts b/packages/holodeck/client/index.ts index 79ab456c01..77eec7fff6 100644 --- a/packages/holodeck/client/index.ts +++ b/packages/holodeck/client/index.ts @@ -1,4 +1,4 @@ export { GET, PATCH, POST, PUT, DELETE, QUERY } from './macros'; export { setTestId } from './env'; export { mock } from './mock'; -export { MockServerHandler } from './handler'; +export { HolodeckHandler } from './handler'; diff --git a/packages/holodeck/docs/basic-concepts.md b/packages/holodeck/docs/basic-concepts.md new file mode 100644 index 0000000000..018dae1656 --- /dev/null +++ b/packages/holodeck/docs/basic-concepts.md @@ -0,0 +1,20 @@ +## Usage +#### Mocking from Within a Test + +```ts +import { GET } from '@warp-drive/holodeck/mock'; + +await GET(context, 'users/1', () => ({ + data: { + id: '1', + type: 'user', + attributes: { + name: 'Chris Thoburn', + }, + }, + +// set RECORD to false or remove +// the options hash entirely once the request +// has been recorded +}), { RECORD: true }); +``` diff --git a/packages/holodeck/docs/motivations.md b/packages/holodeck/docs/motivations.md new file mode 100644 index 0000000000..559bec0a69 --- /dev/null +++ b/packages/holodeck/docs/motivations.md @@ -0,0 +1,21 @@ +# About + +We believe in robust fast test suites. WarpDrive provides the patterns for how you consume and manage state in your app, and as a data-framework that extends to ensuring those patterns are rigourously testable. + +By providing a mocking library with tight integration with WarpDrive's concepts we make it easier and more accurate than ever to mock data and requests, while keeping your test suite blazing fast. + +Using Holodeck you will find you write and maintain better tests faster than ever. + +### ✨ Amazing Developer Experience + +WarpDrive already understands your data schemas and request patterns. Building a mocking utility with tight integration into your data usage patterns could bring enormous DX and test suite performance benefits. + +Building a real mock server instead of intercepting requests in the browser or via ServiceWorker gives us out-of-the-box DX, better tunability, and greater ability to optimize test suite performance. Speed is the ultimate DX. + +### 🔥 Blazing Fast Tests + +We've noticed test suites spending an enormous amount of time creating and tearing down mock state in between tests. To combat this, we want to provide +an approach built over `http/3` (`http/2` for now) utilizing aggressive caching +and `brotli` minification in a way that can be replayed over and over again. + +Basically, pay the cost when you write the test. Forever after skip the cost until you need to edit the test again. diff --git a/packages/holodeck/docs/request-integration.md b/packages/holodeck/docs/request-integration.md new file mode 100644 index 0000000000..68667d9ec9 --- /dev/null +++ b/packages/holodeck/docs/request-integration.md @@ -0,0 +1,37 @@ +# Request Integration + +In order to properly manage test isolation holodeck intercepts and decorates requests. For this, we take advantage of each test context typically having its own WarpDrive RequestManager instance. + +You should add the HolodeckHandler to the RequestManager chain prior to `Fetch` (or any equivalent handler that proceeds to network). + +From within a test this might look like: + +```ts +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; +import { HolodeckHandler } from '@warp-drive/holodeck'; +import { module, test } from 'qunit'; + +module('my module', function() { + test('my test', async function() { + const manager = new RequestManager() + .use([new HolodeckHandler(this), Fetch]); + }); +}); +``` + +We can use the `isTesting` macro from [@embroider/macros]() to add this handler into our chain all the time to ease test setup: + +```ts + const manager = new RequestManager() + .use( + [ + // only include the HolodeckHandler in testing envs + macroCondition( + isTesting() + ) ? new HolodeckHandler(this) : false, + Fetch + ].filter(Boolean) + ); +``` + diff --git a/packages/holodeck/docs/server-setup.md b/packages/holodeck/docs/server-setup.md new file mode 100644 index 0000000000..dcf049869d --- /dev/null +++ b/packages/holodeck/docs/server-setup.md @@ -0,0 +1,73 @@ +# Server Setup + +The holodeck server can be easily integrated into any testing process. Below we show integration with either Testem and Diagnostic. + +## Testem + +You can integrate holodeck with Testem using testem's [async config capability](https://github.com/testem/testem/blob/master/docs/config_file.md#returning-a-promise-from-testemjs): + +```ts +module.exports = async function () { + const holodeck = (await import('@warp-drive/holodeck/server')).default; + await holodeck.launchProgram({ + port: 7373, + }); + + process.on('beforeExit', async () => { + await holodeck.endProgram(); + }); + + return { + // ... testem config + }; +}; +``` + +If you need the API mock to run on the same port as the test suite, you can use Testem's [API Proxy](https://github.com/testem/testem/tree/master?tab=readme-ov-file#api-proxy) + +```ts +module.exports = async function () { + const holodeck = (await import('@warp-drive/holodeck/server')).default; + await holodeck.launchProgram({ + port: 7373, + }); + + process.on('beforeExit', async () => { + await holodeck.endProgram(); + }); + + return { + "proxies": { + "/api": { + // holodeck always runs on https + // the proxy is transparent so this means /api/v1 will route to https://localhost:7373/api/v1 + "target": "https://localhost:7373", + // "onlyContentTypes": ["xml", "json"], + // if test suite is on http, set this to false + // "secure": false, + }, + } + }; +}; +``` + +## Diagnostic + +holodeck can be launched and cleaned up using the lifecycle hooks in the launch config +for diagnostic in `diagnostic.js`: + +```ts +import launch from '@warp-drive/diagnostic/server/default-setup.js'; +import holodeck from '@warp-drive/holodeck/server'; + +await launch({ + async setup(options) { + await holodeck.launchProgram({ + port: options.port + 1, + }); + }, + async cleanup() { + await holodeck.endProgram(); + }, +}); +``` diff --git a/packages/holodeck/docs/test-framework-integration.md b/packages/holodeck/docs/test-framework-integration.md new file mode 100644 index 0000000000..cdc8728c6a --- /dev/null +++ b/packages/holodeck/docs/test-framework-integration.md @@ -0,0 +1,55 @@ +# Test Framework Integration + +You will need to configure holodeck to understand your test context and port. + +> [!TIP] +> For qunit and diagnostic in a project using Ember this is typically done in `tests/test-helper.js` + +## setTestId + +As part of its strategy for enabling fast test suites, Holodeck utilizes a real http server and scopes requests to individual test contexts. + +This allows tests to run concurrently even when they make similar or identical requests without leaking state between. + +To do this, we give holodeck a unique stable testId for each test context. Below we show how to achieve +this with several common test frameworks. + +### With QUnit + +```ts +QUnit.hooks.beforeEach(function (assert) { + setTestId(this, assert.test.testId); +}); +QUnit.hooks.afterEach(function (assert) { + setTestId(this, null); +}); +``` + +### With Diagnostic + +```ts +import { setupGlobalHooks } from '@warp-drive/diagnostic'; +import { setTestId } from '@warp-drive/holodeck'; + +setupGlobalHooks((hooks) => { + hooks.beforeEach(function (assert) { + setTestId(this, assert.test.testId); + }); + hooks.afterEach(function () { + setTestId(this, null); + }); +}); +``` + +## setConfig + +If our holodeck server is not running on the same host and port as our application we need to tell it where to direct requests to. + +```ts +import { setConfig } from '@warp-drive/holodeck'; + +// if not proxying the port / set port to the correct value here. For instance if we always run our tests on port N and holodeck on port N +1 we could do that like below. +const MockHost = `https://${window.location.hostname}:${Number(window.location.port) + 1}`; + +setConfig({ host: MockHost }); +``` diff --git a/packages/holodeck/server/index.ts b/packages/holodeck/server/index.ts index 33a386cc61..642604ae75 100644 --- a/packages/holodeck/server/index.ts +++ b/packages/holodeck/server/index.ts @@ -169,7 +169,7 @@ function createTestHandler(projectRoot: string): MiddlewareHandler { code: 'MISSING_X_TEST_ID_HEADER', title: 'Request to the http mock server is missing the `X-Test-Id` header', detail: - "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.", + "The `X-Test-Id` header is used to identify the test that is making the request to the mock server. This is used to ensure that the mock server is only used for the test that is currently running. If using @ember-data/request add import { HolodeckHandler } from '@warp-drive/holodeck'; to your request handlers.", source: { header: 'X-Test-Id' }, }, ], @@ -188,7 +188,7 @@ function createTestHandler(projectRoot: string): MiddlewareHandler { code: 'MISSING_X_TEST_REQUEST_NUMBER_HEADER', title: 'Request to the http mock server is missing the `X-Test-Request-Number` header', detail: - "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { MockServerHandler } from '@warp-drive/holodeck'; to your request handlers.", + "The `X-Test-Request-Number` header is used to identify the request number for the current test. This is used to ensure that the mock server response is deterministic for the test that is currently running. If using @ember-data/request add import { HolodeckHandler } from '@warp-drive/holodeck'; to your request handlers.", source: { header: 'X-Test-Request-Number' }, }, ], diff --git a/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts index 143e01776c..5c4b097276 100644 --- a/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/error-documents-test.ts @@ -5,7 +5,7 @@ import { buildBaseURL } from '@ember-data/request-utils'; import Store, { CacheHandler } from '@ember-data/store'; import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import { module, test } from '@warp-drive/diagnostic'; -import { mock, MockServerHandler } from '@warp-drive/holodeck'; +import { HolodeckHandler, mock } from '@warp-drive/holodeck'; const RECORD = false; @@ -34,7 +34,7 @@ module('Integration | @ember-data/json-api Cach.put()', function const manager = new RequestManager(); const store = new TestStore(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); manager.useCache(CacheHandler); store.requestManager = manager; diff --git a/tests/ember-data__request/tests/integration/fetch-handler-test.ts b/tests/ember-data__request/tests/integration/fetch-handler-test.ts index 1c7ddd9201..4124e6decd 100644 --- a/tests/ember-data__request/tests/integration/fetch-handler-test.ts +++ b/tests/ember-data__request/tests/integration/fetch-handler-test.ts @@ -2,7 +2,7 @@ import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import { buildBaseURL } from '@ember-data/request-utils'; import { module, test } from '@warp-drive/diagnostic'; -import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler, mock } from '@warp-drive/holodeck'; const RECORD = false; @@ -23,7 +23,7 @@ function isNetworkError(e: unknown): asserts e is Error & { module('RequestManager | Fetch Handler', function (hooks) { test('Parses 200 Responses', async function (assert) { const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); await GET( this, @@ -83,7 +83,7 @@ module('RequestManager | Fetch Handler', function (hooks) { test('It provides useful errors', async function (assert) { const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); await mock( this, @@ -156,7 +156,7 @@ module('RequestManager | Fetch Handler', function (hooks) { test('It provides useful error during abort', async function (assert) { const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); await GET( this, diff --git a/tests/warp-drive__ember/tests/integration/get-request-state-test.gts b/tests/warp-drive__ember/tests/integration/get-request-state-test.gts index d5e58d92a5..148f5d3481 100644 --- a/tests/warp-drive__ember/tests/integration/get-request-state-test.gts +++ b/tests/warp-drive__ember/tests/integration/get-request-state-test.gts @@ -7,7 +7,7 @@ import { buildBaseURL } from '@ember-data/request-utils'; import type { RenderingTestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { getRequestState } from '@warp-drive/ember'; -import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler, mock } from '@warp-drive/holodeck'; type RequestState = ReturnType>; type UserResource = { @@ -99,7 +99,7 @@ module('Integration | get-request-state', function (hooks) { hooks.beforeEach(function () { const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); manager.useCache(new SimpleCacheHandler()); this.manager = manager; diff --git a/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts b/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts index 892e5c3afe..d22cb7384c 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-invalidation-test.gts @@ -14,7 +14,7 @@ import type { Diagnostic } from '@warp-drive/diagnostic/-types'; import type { RenderingTestContext, TestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { Request } from '@warp-drive/ember'; -import { GET, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; @@ -52,7 +52,7 @@ class Logger implements Handler { class TestStore extends Store { setupRequestManager(testContext: TestContext, assert: Diagnostic): void { this.requestManager = new RequestManager() - .use([new Logger(assert), new MockServerHandler(testContext), Fetch]) + .use([new Logger(assert), new HolodeckHandler(testContext), Fetch]) .useCache(CacheHandler); } diff --git a/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts b/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts index a592c14d3b..72e2e3f616 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-subscription-test.gts @@ -14,7 +14,7 @@ import type { Diagnostic } from '@warp-drive/diagnostic/-types'; import type { RenderingTestContext, TestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { Request } from '@warp-drive/ember'; -import { GET, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService, withDefaults } from '@warp-drive/schema-record/schema'; @@ -80,7 +80,7 @@ class TestStore extends Store { setupRequestManager(testContext: TestContext, assert: Diagnostic): Logger { const logger = new Logger(assert); this.requestManager = new RequestManager() - .use([logger, new MockServerHandler(testContext), Fetch]) + .use([logger, new HolodeckHandler(testContext), Fetch]) .useCache(CacheHandler); return logger; } diff --git a/tests/warp-drive__ember/tests/integration/request-component-test.gts b/tests/warp-drive__ember/tests/integration/request-component-test.gts index d749b55f9e..53fe087471 100644 --- a/tests/warp-drive__ember/tests/integration/request-component-test.gts +++ b/tests/warp-drive__ember/tests/integration/request-component-test.gts @@ -14,7 +14,7 @@ import type { SingleResourceDataDocument } from '@warp-drive/core-types/spec/doc import type { RenderingTestContext } from '@warp-drive/diagnostic/ember'; import { module, setupRenderingTest, test as _test } from '@warp-drive/diagnostic/ember'; import { getRequestState, Request } from '@warp-drive/ember'; -import { GET, mock, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler, mock } from '@warp-drive/holodeck'; import { registerDerivations, withDefaults } from '@warp-drive/schema-record/schema'; // our tests use a rendering test context and add manager to it @@ -153,7 +153,7 @@ module('Integration | ', function (hooks) { hooks.beforeEach(function () { const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); manager.useCache(new SimpleCacheHandler()); this.manager = manager; @@ -402,7 +402,7 @@ module('Integration | ', function (hooks) { test('externally retriggered request works as expected (store CacheHandler)', async function (assert) { const store = this.owner.lookup('service:store') as Store; const manager = new RequestManager(); - manager.use([new MockServerHandler(this), Fetch]); + manager.use([new HolodeckHandler(this), Fetch]); manager.useCache(StoreHandler); store.requestManager = manager; this.manager = manager; diff --git a/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts b/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts index 70f726a858..2982ac1503 100644 --- a/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts +++ b/tests/warp-drive__experiments/tests/integration/data-worker/basic-test.ts @@ -6,7 +6,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { module, test } from '@warp-drive/diagnostic'; import { WorkerFetch } from '@warp-drive/experiments/worker-fetch'; -import { GET, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService } from '@warp-drive/schema-record/schema'; @@ -18,7 +18,7 @@ const RECORD = true; module('Unit | DataWorker | Basic', function (_hooks) { test('it exists', async function (assert) { const worker = new Worker(new URL('./basic-worker.ts', import.meta.url)); - const MockHandler = new MockServerHandler(this); + const MockHandler = new HolodeckHandler(this); await GET( this, diff --git a/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts b/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts index bd26be2b94..32197e6f01 100644 --- a/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts +++ b/tests/warp-drive__experiments/tests/integration/data-worker/serialization-test.ts @@ -6,7 +6,7 @@ import type { CacheCapabilitiesManager } from '@ember-data/store/types'; import type { StableRecordIdentifier } from '@warp-drive/core-types'; import { module, test } from '@warp-drive/diagnostic'; import { WorkerFetch } from '@warp-drive/experiments/worker-fetch'; -import { GET, MockServerHandler } from '@warp-drive/holodeck'; +import { GET, HolodeckHandler } from '@warp-drive/holodeck'; import { instantiateRecord, teardownRecord } from '@warp-drive/schema-record/hooks'; import type { SchemaRecord } from '@warp-drive/schema-record/record'; import { registerDerivations, SchemaService } from '@warp-drive/schema-record/schema'; @@ -27,7 +27,7 @@ const RECORD = true; module('Unit | DataWorker | Serialization & Persistence', function (_hooks) { test('Serialization of Request/Response/Headers works as expected', async function (assert) { const worker = new Worker(new URL('./persisted-worker.ts', import.meta.url)); - const MockHandler = new MockServerHandler(this); + const MockHandler = new HolodeckHandler(this); await GET( this, From 4260af9f2913f624095cc75f585f88021937ebec Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 8 Dec 2024 15:21:18 -0800 Subject: [PATCH 8/8] fixup imports --- tests/main/testem.js | 2 +- tests/main/tsconfig.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/main/testem.js b/tests/main/testem.js index 792fa67528..ac394fe13d 100644 --- a/tests/main/testem.js +++ b/tests/main/testem.js @@ -1,7 +1,7 @@ const TestemConfig = require('@ember-data/unpublished-test-infra/testem/testem'); module.exports = async function () { - const holodeck = (await import('@warp-drive/holodeck')).default; + const holodeck = (await import('@warp-drive/holodeck/server')).default; await holodeck.launchProgram({ port: 7373, }); diff --git a/tests/main/tsconfig.json b/tests/main/tsconfig.json index 32a05641fb..e71104dbf9 100644 --- a/tests/main/tsconfig.json +++ b/tests/main/tsconfig.json @@ -57,8 +57,8 @@ "@warp-drive/schema-record/*": ["../../packages/schema-record/unstable-preview-types/*"], "@warp-drive/build-config": ["../../packages/build-config/unstable-preview-types"], "@warp-drive/build-config/*": ["../../packages/build-config/unstable-preview-types/*"], - "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types"], - "@warp-drive/holodeck/*": ["../../packages/holodeck/unstable-preview-types/*"], + "@warp-drive/holodeck": ["../../packages/holodeck/unstable-preview-types/client"], + "@warp-drive/holodeck/*": ["../../packages/holodeck/unstable-preview-types/client/*"], "ember-data": ["../../packages/-ember-data/unstable-preview-types"], "ember-data/*": ["../../packages/-ember-data/unstable-preview-types/*"] }