@stratal/testing 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/dist/core/env/index.d.ts +2 -0
- package/dist/core/env/index.d.ts.map +1 -0
- package/dist/core/env/index.js +2 -0
- package/dist/core/env/index.js.map +1 -0
- package/dist/core/env/test-env.d.ts +9 -0
- package/dist/core/env/test-env.d.ts.map +1 -0
- package/dist/core/env/test-env.js +14 -0
- package/dist/core/env/test-env.js.map +1 -0
- package/dist/core/http/fetch-mock.d.ts +236 -0
- package/dist/core/http/fetch-mock.d.ts.map +1 -0
- package/dist/core/http/fetch-mock.js +290 -0
- package/dist/core/http/fetch-mock.js.map +1 -0
- package/dist/core/http/fetch-mock.types.d.ts +48 -0
- package/dist/core/http/fetch-mock.types.d.ts.map +1 -0
- package/dist/core/http/fetch-mock.types.js +2 -0
- package/dist/core/http/fetch-mock.types.js.map +1 -0
- package/dist/core/http/index.d.ts +6 -0
- package/dist/core/http/index.d.ts.map +1 -0
- package/dist/core/http/index.js +5 -0
- package/dist/core/http/index.js.map +1 -0
- package/dist/core/http/test-http-client.d.ts +54 -0
- package/dist/core/http/test-http-client.d.ts.map +1 -0
- package/dist/core/http/test-http-client.js +75 -0
- package/dist/core/http/test-http-client.js.map +1 -0
- package/dist/core/http/test-http-request.d.ts +44 -0
- package/dist/core/http/test-http-request.d.ts.map +1 -0
- package/dist/core/http/test-http-request.js +75 -0
- package/dist/core/http/test-http-request.js.map +1 -0
- package/dist/core/http/test-response.d.ts +161 -0
- package/dist/core/http/test-response.d.ts.map +1 -0
- package/dist/core/http/test-response.js +309 -0
- package/dist/core/http/test-response.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +7 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/override/index.d.ts +2 -0
- package/dist/core/override/index.d.ts.map +1 -0
- package/dist/core/override/index.js +2 -0
- package/dist/core/override/index.js.map +1 -0
- package/dist/core/override/provider-override-builder.d.ts +77 -0
- package/dist/core/override/provider-override-builder.d.ts.map +1 -0
- package/dist/core/override/provider-override-builder.js +94 -0
- package/dist/core/override/provider-override-builder.js.map +1 -0
- package/dist/core/test.d.ts +48 -0
- package/dist/core/test.d.ts.map +1 -0
- package/dist/core/test.js +53 -0
- package/dist/core/test.js.map +1 -0
- package/dist/core/testing-module-builder.d.ts +99 -0
- package/dist/core/testing-module-builder.d.ts.map +1 -0
- package/dist/core/testing-module-builder.js +156 -0
- package/dist/core/testing-module-builder.js.map +1 -0
- package/dist/core/testing-module.d.ts +79 -0
- package/dist/core/testing-module.d.ts.map +1 -0
- package/dist/core/testing-module.js +99 -0
- package/dist/core/testing-module.js.map +1 -0
- package/dist/errors/index.d.ts +3 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/setup-error.d.ts +9 -0
- package/dist/errors/setup-error.d.ts.map +1 -0
- package/dist/errors/setup-error.js +11 -0
- package/dist/errors/setup-error.js.map +1 -0
- package/dist/errors/test-error.d.ts +9 -0
- package/dist/errors/test-error.d.ts.map +1 -0
- package/dist/errors/test-error.js +15 -0
- package/dist/errors/test-error.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/mocks/index.d.ts +4 -0
- package/dist/mocks/index.d.ts.map +1 -0
- package/dist/mocks/index.js +4 -0
- package/dist/mocks/index.js.map +1 -0
- package/dist/mocks/nodemailer.d.ts +10 -0
- package/dist/mocks/nodemailer.d.ts.map +1 -0
- package/dist/mocks/nodemailer.js +9 -0
- package/dist/mocks/nodemailer.js.map +1 -0
- package/dist/storage/fake-storage.service.d.ts +114 -0
- package/dist/storage/fake-storage.service.d.ts.map +1 -0
- package/dist/storage/fake-storage.service.js +233 -0
- package/dist/storage/fake-storage.service.js.map +1 -0
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -0
- package/package.json +10 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @stratal/testing
|
|
2
|
+
|
|
3
|
+
## 0.0.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- **Build cleanup** — Removed redundant polyfills export and cleaned up the build configuration.
|
|
8
|
+
- **Relaxed vitest version** — Loosened the vitest peer dependency version constraint.
|
|
9
|
+
|
|
10
|
+
- Updated dependencies []:
|
|
11
|
+
- stratal@0.0.4
|
|
12
|
+
|
|
13
|
+
## 0.0.3
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- - **Build cleanup** — Removed redundant polyfills export and cleaned up the build configuration.
|
|
18
|
+
- **Relaxed vitest version** — Loosened the vitest peer dependency version constraint.
|
|
19
|
+
- Updated dependencies []:
|
|
20
|
+
- stratal@0.0.3
|
|
21
|
+
|
|
22
|
+
## 0.0.2
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
#### `stratal` (core)
|
|
27
|
+
|
|
28
|
+
##### Breaking Changes
|
|
29
|
+
|
|
30
|
+
- **`withRoot`/`withRootAsync` renamed to `forRoot`/`forRootAsync`** — All dynamic module configuration methods have been renamed for consistency. Update every `SomeModule.withRoot(...)` call to `SomeModule.forRoot(...)` and `SomeModule.withRootAsync(...)` to `SomeModule.forRootAsync(...)`. ([`152913a`](https://github.com/strataljs/stratal/commit/152913a))
|
|
31
|
+
|
|
32
|
+
- **Barrel export removed — use sub-path imports** — The top-level `stratal` barrel export has been removed. Consumers must now import from sub-paths (`stratal/di`, `stratal/router`, `stratal/cache`, `stratal/validation`, etc.). ([`af073d8`](https://github.com/strataljs/stratal/commit/af073d8))
|
|
33
|
+
|
|
34
|
+
##### Features
|
|
35
|
+
|
|
36
|
+
- **DOM polyfill for Cloudflare Workers** — Added a DOM polyfill to support AWS SDK v3 XML parsing in Cloudflare Workers environments. ([`f3b2cb9`](https://github.com/strataljs/stratal/commit/f3b2cb9))
|
|
37
|
+
|
|
38
|
+
- **Benchmark suite** — Added benchmark functionality for measuring framework performa30438`](https://github.com/strataljs/stratal/commit/7230438))
|
|
39
|
+
|
|
40
|
+
##### Security
|
|
41
|
+
|
|
42
|
+
- **ConfigService blocks prototype pollution** — `ConfigService` now rejects dangerous keys (`__proto__`, `constructor`, `prototype`) to prevent prototype pollution attacks. ([`567139c`](https://github.com/strataljs/stratal/commit/567139c), [`e64b4e7`](https://github.com/strataljs/stratal/commit/e64b4e7))
|
|
43
|
+
|
|
44
|
+
##### Bug Fixes
|
|
45
|
+
|
|
46
|
+
- **`reflect-metadata` import moved to vitest setup** — Removed unused `reflect-metadata` imports from example files and centralized the import in `vitest.setup.ts`. ([`f3b2cb9`](https://github.com/strataljs/stratal/commit/f3b2cb9))
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
#### `@stratal/testing`
|
|
51
|
+
|
|
52
|
+
##### Breaking Changes
|
|
53
|
+
|
|
54
|
+
- **Import paths updated** — Import paths updated to match the new core sub-path exports (e.g. `stratal/di` instead of the barrel `stratal`). ([`af073d8`](https://github.com/strataljs/stratal/commit/af073d8))
|
|
55
|
+
|
|
56
|
+
- Updated dependencies []:
|
|
57
|
+
- @stratal/testing@0.0.2
|
|
58
|
+
|
|
59
|
+
## 0.0.1
|
|
60
|
+
|
|
61
|
+
### Patch Changes
|
|
62
|
+
|
|
63
|
+
- Initial release of the Stratal framework — a modular Cloudflare Workers framework built on Hono and tsyringe.
|
|
64
|
+
|
|
65
|
+
**Core Infrastructure**
|
|
66
|
+
|
|
67
|
+
- NestJS-style module system with `@Module()` decorator, dynamic modules (`forRoot`, `forRootAsync`), and lifecycle hooks (`OnInitialize`, `OnShutdown`)
|
|
68
|
+
- Two-tier dependency injection container (global singletons + request-scoped) powered by tsyringe with conditional registration and service decoration
|
|
69
|
+
- `StratalWorker` entry point extending Cloudflare's `WorkerEntrypoint` for HTTP fetch, queue batches, and scheduled cron triggers
|
|
70
|
+
|
|
71
|
+
**Routing & API**
|
|
72
|
+
|
|
73
|
+
- Hono-based routing with `@Controller()` and `@Route()` decorators, automatic controller discovery, and route guards via `@UseGuards()`
|
|
74
|
+
- OpenAPI schema generation with `@hono/zod-openapi` and Scalar API reference integration
|
|
75
|
+
- NestJS-like middleware configuration with route-specific application and exclusion
|
|
76
|
+
|
|
77
|
+
**Background Processing**
|
|
78
|
+
|
|
79
|
+
- Queue consumerfor Cloudflare Queues with `@Consumer()` and `@QueueJob()` decorators and batch processing
|
|
80
|
+
- Cron job scheduling via `CronManager` integrated with Cloudflare's scheduled events
|
|
81
|
+
|
|
82
|
+
**Services & Integrations**
|
|
83
|
+
|
|
84
|
+
- Email module with pluggable providers (Nodemailer, Resend) and queue-based sending
|
|
85
|
+
- Storage module with AWS S3 / Cloudflare R2 support, multipart uploads, presigned URLs, and TUS resumable uploads
|
|
86
|
+
- Internationalization (i18n) module with locale detection, message compilation, and request-scoped translations
|
|
87
|
+
- Cache module with pluggable providers and Cloudflare KV integration
|
|
88
|
+
- Configuration module with `registerAs()` namespaces and Zod-based validation
|
|
89
|
+
- Structured logging with JSON and pretty formatters
|
|
90
|
+
|
|
91
|
+
**Developer Experience**
|
|
92
|
+
|
|
93
|
+
- Zod-powered request/response validation with type inference
|
|
94
|
+
- Custom `ApplicationError` class with HTTP status mapping
|
|
95
|
+
- ESM-only with full TypeScript decorator support (`emitDecoratorMetadata`)
|
|
96
|
+
- Sub-path exports for tree-shakeable imports (`stratal/di`, `stratal/router`, `stratal/cache`, etc.)
|
|
97
|
+
|
|
98
|
+
- `TestingModule` and `TestingModuleBuilder` for bootstrapping isolated module environments in tests
|
|
99
|
+
- `TestHttpClient` with request builder and response wrapper for integration testing
|
|
100
|
+
- `FetchMock` for mocking HTTP fetch calls
|
|
101
|
+
- `FakeStorageService` for in-memory storage testing without S3/R2
|
|
102
|
+
- `ProviderOverrideBuilder` for replacing providers with test doubles
|
|
103
|
+
- Nodemailer mock for email testing
|
|
104
|
+
- Test environment utilities via `getTestEnv()`
|
|
105
|
+
|
|
106
|
+
- Updated dependencies []:
|
|
107
|
+
- stratal@0.0.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/env/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/env/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type StratalEnv } from 'stratal';
|
|
2
|
+
/**
|
|
3
|
+
* Get test environment with optional overrides
|
|
4
|
+
*
|
|
5
|
+
* @param overrides - Optional partial env to merge with cloudflare:test env
|
|
6
|
+
* @returns Complete Env object for testing
|
|
7
|
+
*/
|
|
8
|
+
export declare function getTestEnv(overrides?: Partial<StratalEnv>): StratalEnv;
|
|
9
|
+
//# sourceMappingURL=test-env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-env.d.ts","sourceRoot":"","sources":["../../../src/core/env/test-env.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAKtE"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { env as cloudflareEnv } from 'cloudflare:test';
|
|
2
|
+
/**
|
|
3
|
+
* Get test environment with optional overrides
|
|
4
|
+
*
|
|
5
|
+
* @param overrides - Optional partial env to merge with cloudflare:test env
|
|
6
|
+
* @returns Complete Env object for testing
|
|
7
|
+
*/
|
|
8
|
+
export function getTestEnv(overrides) {
|
|
9
|
+
return {
|
|
10
|
+
...cloudflareEnv,
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=test-env.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-env.js","sourceRoot":"","sources":["../../../src/core/env/test-env.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,GAAG,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,SAA+B;IACxD,OAAO;QACL,GAAG,aAAa;QAChB,GAAG,SAAS;KACC,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { MockJsonOptions, MockErrorOptions } from './fetch-mock.types';
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper around Cloudflare's fetchMock for declarative fetch mocking in tests
|
|
4
|
+
*
|
|
5
|
+
* Based on undici's MockAgent, fetchMock.get(origin) returns a MockPool for that origin.
|
|
6
|
+
* The MockPool's intercept() method is used to define which requests to mock.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createFetchMock } from '@stratal/testing'
|
|
11
|
+
*
|
|
12
|
+
* const mock = createFetchMock()
|
|
13
|
+
*
|
|
14
|
+
* beforeEach(() => {
|
|
15
|
+
* mock.activate()
|
|
16
|
+
* mock.disableNetConnect()
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* afterEach(() => {
|
|
20
|
+
* mock.reset()
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* it('should mock external API', async () => {
|
|
24
|
+
* // Using helper method
|
|
25
|
+
* mock.mockJsonResponse('https://api.example.com/data', { success: true })
|
|
26
|
+
*
|
|
27
|
+
* // Or using direct API
|
|
28
|
+
* mock.get('https://api.example.com')
|
|
29
|
+
* .intercept({ path: '/users', method: 'POST' })
|
|
30
|
+
* .reply(201, JSON.stringify({ created: true }))
|
|
31
|
+
*
|
|
32
|
+
* const response = await fetch('https://api.example.com/data')
|
|
33
|
+
* const json = await response.json()
|
|
34
|
+
*
|
|
35
|
+
* expect(json.success).toBe(true)
|
|
36
|
+
* mock.assertNoPendingInterceptors()
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare class FetchMock {
|
|
41
|
+
/**
|
|
42
|
+
* Get a MockPool for the specified origin
|
|
43
|
+
*
|
|
44
|
+
* This is the underlying fetchMock.get() method that returns a MockPool
|
|
45
|
+
* for mocking requests to the specified origin.
|
|
46
|
+
*
|
|
47
|
+
* @param origin - The origin URL (e.g., 'https://api.example.com')
|
|
48
|
+
* @returns MockPool for chaining .intercept() and .reply()
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* // Mock a GET request
|
|
53
|
+
* mock.get('https://api.example.com')
|
|
54
|
+
* .intercept({ path: '/users', method: 'GET' })
|
|
55
|
+
* .reply(200, JSON.stringify({ users: [] }))
|
|
56
|
+
*
|
|
57
|
+
* // Mock a POST request with body matching
|
|
58
|
+
* mock.get('https://api.example.com')
|
|
59
|
+
* .intercept({
|
|
60
|
+
* path: '/users',
|
|
61
|
+
* method: 'POST',
|
|
62
|
+
* body: (body) => body.includes('test')
|
|
63
|
+
* })
|
|
64
|
+
* .reply(201, JSON.stringify({ created: true }))
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
get(origin: string): import("cloudflare:test").Interceptable;
|
|
68
|
+
/**
|
|
69
|
+
* Activate fetch mocking for the current test
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* beforeEach(() => {
|
|
74
|
+
* mock.activate()
|
|
75
|
+
* })
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
activate(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Deactivate fetch mocking
|
|
81
|
+
*/
|
|
82
|
+
deactivate(): void;
|
|
83
|
+
/**
|
|
84
|
+
* Disable all network connections, forcing all requests to use mocks
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* beforeEach(() => {
|
|
89
|
+
* mock.activate()
|
|
90
|
+
* mock.disableNetConnect() // Ensure all requests are mocked
|
|
91
|
+
* })
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
disableNetConnect(): void;
|
|
95
|
+
/**
|
|
96
|
+
* Enable network connections for specific hosts
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* mock.enableNetConnect('localhost')
|
|
101
|
+
* mock.enableNetConnect(/^https:\/\/trusted\.com/)
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
enableNetConnect(host?: string | RegExp): void;
|
|
105
|
+
/**
|
|
106
|
+
* Assert that all defined interceptors were called
|
|
107
|
+
*
|
|
108
|
+
* @throws {Error} If there are pending interceptors that weren't matched
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* it('should call all mocked endpoints', async () => {
|
|
113
|
+
* mock.mockJsonResponse('https://api.example.com/data', { data: [] })
|
|
114
|
+
*
|
|
115
|
+
* await fetch('https://api.example.com/data')
|
|
116
|
+
*
|
|
117
|
+
* mock.assertNoPendingInterceptors() // Pass
|
|
118
|
+
* })
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
assertNoPendingInterceptors(): void;
|
|
122
|
+
/**
|
|
123
|
+
* Reset all mocks and interceptors
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* afterEach(() => {
|
|
128
|
+
* mock.reset()
|
|
129
|
+
* })
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
reset(): void;
|
|
133
|
+
/**
|
|
134
|
+
* Helper method to mock JSON responses
|
|
135
|
+
*
|
|
136
|
+
* Automatically parses the URL into origin and path, and sets up the mock.
|
|
137
|
+
*
|
|
138
|
+
* @param url - Full URL to mock (e.g., 'https://api.example.com/users')
|
|
139
|
+
* @param data - JSON data to return
|
|
140
|
+
* @param options - Additional options for the mock
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* // Mock a GET request
|
|
145
|
+
* mock.mockJsonResponse('https://api.example.com/users', { users: [] })
|
|
146
|
+
*
|
|
147
|
+
* // Mock a POST request
|
|
148
|
+
* mock.mockJsonResponse(
|
|
149
|
+
* 'https://api.example.com/users',
|
|
150
|
+
* { created: true },
|
|
151
|
+
* { method: 'POST', status: 201 }
|
|
152
|
+
* )
|
|
153
|
+
*
|
|
154
|
+
* // With custom headers and delay
|
|
155
|
+
* mock.mockJsonResponse(
|
|
156
|
+
* 'https://api.example.com/users',
|
|
157
|
+
* { users: [] },
|
|
158
|
+
* {
|
|
159
|
+
* status: 200,
|
|
160
|
+
* method: 'GET',
|
|
161
|
+
* headers: { 'X-Custom': 'value' },
|
|
162
|
+
* delay: 100
|
|
163
|
+
* }
|
|
164
|
+
* )
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
mockJsonResponse(url: string, data: unknown, options?: MockJsonOptions): import("cloudflare:test").MockScope<object>;
|
|
168
|
+
/**
|
|
169
|
+
* Helper method to mock error responses
|
|
170
|
+
*
|
|
171
|
+
* @param url - Full URL to mock
|
|
172
|
+
* @param status - HTTP error status code
|
|
173
|
+
* @param message - Optional error message
|
|
174
|
+
* @param options - Additional options for the error mock
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* // Mock a 401 error
|
|
179
|
+
* mock.mockError('https://api.example.com/fail', 401, 'Unauthorized')
|
|
180
|
+
*
|
|
181
|
+
* // Mock a 500 error with custom method
|
|
182
|
+
* mock.mockError(
|
|
183
|
+
* 'https://api.example.com/fail',
|
|
184
|
+
* 500,
|
|
185
|
+
* 'Server Error',
|
|
186
|
+
* { method: 'POST' }
|
|
187
|
+
* )
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
mockError(url: string, status: number, message?: string, options?: MockErrorOptions): import("cloudflare:test").MockScope<object>;
|
|
191
|
+
/**
|
|
192
|
+
* Generic helper to mock any HTTP request
|
|
193
|
+
*
|
|
194
|
+
* @param origin - The origin URL (e.g., 'https://api.example.com')
|
|
195
|
+
* @param options - Request matching and response options
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* mock.mockRequest('https://api.example.com', {
|
|
200
|
+
* path: '/users',
|
|
201
|
+
* method: 'PUT',
|
|
202
|
+
* status: 200,
|
|
203
|
+
* body: { updated: true }
|
|
204
|
+
* })
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
mockRequest(origin: string, options: {
|
|
208
|
+
path: string;
|
|
209
|
+
method?: string;
|
|
210
|
+
status?: number;
|
|
211
|
+
body?: unknown;
|
|
212
|
+
}): import("cloudflare:test").MockScope<object>;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Factory function to create a new FetchMock instance
|
|
216
|
+
*
|
|
217
|
+
* @returns A new FetchMock instance
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```typescript
|
|
221
|
+
* import { createFetchMock } from '@stratal/testing'
|
|
222
|
+
*
|
|
223
|
+
* const mock = createFetchMock()
|
|
224
|
+
*
|
|
225
|
+
* beforeEach(() => {
|
|
226
|
+
* mock.activate()
|
|
227
|
+
* mock.disableNetConnect()
|
|
228
|
+
* })
|
|
229
|
+
*
|
|
230
|
+
* afterEach(() => {
|
|
231
|
+
* mock.reset()
|
|
232
|
+
* })
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export declare function createFetchMock(): FetchMock;
|
|
236
|
+
//# sourceMappingURL=fetch-mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-mock.d.ts","sourceRoot":"","sources":["../../../src/core/http/fetch-mock.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,qBAAa,SAAS;IACrB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM;IAIlB;;;;;;;;;OASG;IACH,QAAQ;IAIR;;OAEG;IACH,UAAU;IAIV;;;;;;;;;;OAUG;IACH,iBAAiB;IAIjB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM;IAOvC;;;;;;;;;;;;;;;OAeG;IACH,2BAA2B;IAI3B;;;;;;;;;OASG;IACH,KAAK;IAKL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACH,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,GAAE,eAAoB;IAwB1E;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;IAkBvF;;;;;;;;;;;;;;;OAeG;IACH,WAAW,CACV,MAAM,EAAE,MAAM,EACd,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE;CAS5E;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,IAAI,SAAS,CAE3C"}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { fetchMock } from 'cloudflare:test';
|
|
2
|
+
/**
|
|
3
|
+
* Wrapper around Cloudflare's fetchMock for declarative fetch mocking in tests
|
|
4
|
+
*
|
|
5
|
+
* Based on undici's MockAgent, fetchMock.get(origin) returns a MockPool for that origin.
|
|
6
|
+
* The MockPool's intercept() method is used to define which requests to mock.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { createFetchMock } from '@stratal/testing'
|
|
11
|
+
*
|
|
12
|
+
* const mock = createFetchMock()
|
|
13
|
+
*
|
|
14
|
+
* beforeEach(() => {
|
|
15
|
+
* mock.activate()
|
|
16
|
+
* mock.disableNetConnect()
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* afterEach(() => {
|
|
20
|
+
* mock.reset()
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* it('should mock external API', async () => {
|
|
24
|
+
* // Using helper method
|
|
25
|
+
* mock.mockJsonResponse('https://api.example.com/data', { success: true })
|
|
26
|
+
*
|
|
27
|
+
* // Or using direct API
|
|
28
|
+
* mock.get('https://api.example.com')
|
|
29
|
+
* .intercept({ path: '/users', method: 'POST' })
|
|
30
|
+
* .reply(201, JSON.stringify({ created: true }))
|
|
31
|
+
*
|
|
32
|
+
* const response = await fetch('https://api.example.com/data')
|
|
33
|
+
* const json = await response.json()
|
|
34
|
+
*
|
|
35
|
+
* expect(json.success).toBe(true)
|
|
36
|
+
* mock.assertNoPendingInterceptors()
|
|
37
|
+
* })
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export class FetchMock {
|
|
41
|
+
/**
|
|
42
|
+
* Get a MockPool for the specified origin
|
|
43
|
+
*
|
|
44
|
+
* This is the underlying fetchMock.get() method that returns a MockPool
|
|
45
|
+
* for mocking requests to the specified origin.
|
|
46
|
+
*
|
|
47
|
+
* @param origin - The origin URL (e.g., 'https://api.example.com')
|
|
48
|
+
* @returns MockPool for chaining .intercept() and .reply()
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* // Mock a GET request
|
|
53
|
+
* mock.get('https://api.example.com')
|
|
54
|
+
* .intercept({ path: '/users', method: 'GET' })
|
|
55
|
+
* .reply(200, JSON.stringify({ users: [] }))
|
|
56
|
+
*
|
|
57
|
+
* // Mock a POST request with body matching
|
|
58
|
+
* mock.get('https://api.example.com')
|
|
59
|
+
* .intercept({
|
|
60
|
+
* path: '/users',
|
|
61
|
+
* method: 'POST',
|
|
62
|
+
* body: (body) => body.includes('test')
|
|
63
|
+
* })
|
|
64
|
+
* .reply(201, JSON.stringify({ created: true }))
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
get(origin) {
|
|
68
|
+
return fetchMock.get(origin);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Activate fetch mocking for the current test
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* beforeEach(() => {
|
|
76
|
+
* mock.activate()
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
activate() {
|
|
81
|
+
fetchMock.activate();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Deactivate fetch mocking
|
|
85
|
+
*/
|
|
86
|
+
deactivate() {
|
|
87
|
+
fetchMock.deactivate();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Disable all network connections, forcing all requests to use mocks
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* beforeEach(() => {
|
|
95
|
+
* mock.activate()
|
|
96
|
+
* mock.disableNetConnect() // Ensure all requests are mocked
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
disableNetConnect() {
|
|
101
|
+
fetchMock.disableNetConnect();
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Enable network connections for specific hosts
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* mock.enableNetConnect('localhost')
|
|
109
|
+
* mock.enableNetConnect(/^https:\/\/trusted\.com/)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
enableNetConnect(host) {
|
|
113
|
+
if (host) {
|
|
114
|
+
fetchMock.enableNetConnect(host);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
fetchMock.enableNetConnect();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Assert that all defined interceptors were called
|
|
121
|
+
*
|
|
122
|
+
* @throws {Error} If there are pending interceptors that weren't matched
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* it('should call all mocked endpoints', async () => {
|
|
127
|
+
* mock.mockJsonResponse('https://api.example.com/data', { data: [] })
|
|
128
|
+
*
|
|
129
|
+
* await fetch('https://api.example.com/data')
|
|
130
|
+
*
|
|
131
|
+
* mock.assertNoPendingInterceptors() // Pass
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
assertNoPendingInterceptors() {
|
|
136
|
+
fetchMock.assertNoPendingInterceptors();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reset all mocks and interceptors
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```typescript
|
|
143
|
+
* afterEach(() => {
|
|
144
|
+
* mock.reset()
|
|
145
|
+
* })
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
reset() {
|
|
149
|
+
// Reset by deactivating
|
|
150
|
+
fetchMock.deactivate();
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Helper method to mock JSON responses
|
|
154
|
+
*
|
|
155
|
+
* Automatically parses the URL into origin and path, and sets up the mock.
|
|
156
|
+
*
|
|
157
|
+
* @param url - Full URL to mock (e.g., 'https://api.example.com/users')
|
|
158
|
+
* @param data - JSON data to return
|
|
159
|
+
* @param options - Additional options for the mock
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* // Mock a GET request
|
|
164
|
+
* mock.mockJsonResponse('https://api.example.com/users', { users: [] })
|
|
165
|
+
*
|
|
166
|
+
* // Mock a POST request
|
|
167
|
+
* mock.mockJsonResponse(
|
|
168
|
+
* 'https://api.example.com/users',
|
|
169
|
+
* { created: true },
|
|
170
|
+
* { method: 'POST', status: 201 }
|
|
171
|
+
* )
|
|
172
|
+
*
|
|
173
|
+
* // With custom headers and delay
|
|
174
|
+
* mock.mockJsonResponse(
|
|
175
|
+
* 'https://api.example.com/users',
|
|
176
|
+
* { users: [] },
|
|
177
|
+
* {
|
|
178
|
+
* status: 200,
|
|
179
|
+
* method: 'GET',
|
|
180
|
+
* headers: { 'X-Custom': 'value' },
|
|
181
|
+
* delay: 100
|
|
182
|
+
* }
|
|
183
|
+
* )
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
mockJsonResponse(url, data, options = {}) {
|
|
187
|
+
const parsedUrl = new URL(url);
|
|
188
|
+
const origin = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
189
|
+
const path = options.path ?? parsedUrl.pathname;
|
|
190
|
+
const method = options.method ?? 'GET';
|
|
191
|
+
const { status = 200, headers = {}, delay } = options;
|
|
192
|
+
const defaultHeaders = {
|
|
193
|
+
'Content-Type': 'application/json',
|
|
194
|
+
...headers,
|
|
195
|
+
};
|
|
196
|
+
const mock = fetchMock
|
|
197
|
+
.get(origin)
|
|
198
|
+
.intercept({ path, method })
|
|
199
|
+
.reply(status, JSON.stringify(data), { headers: defaultHeaders });
|
|
200
|
+
if (delay) {
|
|
201
|
+
return mock.delay(delay);
|
|
202
|
+
}
|
|
203
|
+
return mock;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Helper method to mock error responses
|
|
207
|
+
*
|
|
208
|
+
* @param url - Full URL to mock
|
|
209
|
+
* @param status - HTTP error status code
|
|
210
|
+
* @param message - Optional error message
|
|
211
|
+
* @param options - Additional options for the error mock
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* // Mock a 401 error
|
|
216
|
+
* mock.mockError('https://api.example.com/fail', 401, 'Unauthorized')
|
|
217
|
+
*
|
|
218
|
+
* // Mock a 500 error with custom method
|
|
219
|
+
* mock.mockError(
|
|
220
|
+
* 'https://api.example.com/fail',
|
|
221
|
+
* 500,
|
|
222
|
+
* 'Server Error',
|
|
223
|
+
* { method: 'POST' }
|
|
224
|
+
* )
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
mockError(url, status, message, options = {}) {
|
|
228
|
+
const parsedUrl = new URL(url);
|
|
229
|
+
const origin = `${parsedUrl.protocol}//${parsedUrl.host}`;
|
|
230
|
+
const path = options.path ?? parsedUrl.pathname;
|
|
231
|
+
const method = options.method ?? 'GET';
|
|
232
|
+
const { headers = {} } = options;
|
|
233
|
+
const body = message ? JSON.stringify({ error: message }) : '';
|
|
234
|
+
const responseHeaders = message
|
|
235
|
+
? { 'Content-Type': 'application/json', ...headers }
|
|
236
|
+
: headers;
|
|
237
|
+
return fetchMock
|
|
238
|
+
.get(origin)
|
|
239
|
+
.intercept({ path, method })
|
|
240
|
+
.reply(status, body, { headers: responseHeaders });
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Generic helper to mock any HTTP request
|
|
244
|
+
*
|
|
245
|
+
* @param origin - The origin URL (e.g., 'https://api.example.com')
|
|
246
|
+
* @param options - Request matching and response options
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* mock.mockRequest('https://api.example.com', {
|
|
251
|
+
* path: '/users',
|
|
252
|
+
* method: 'PUT',
|
|
253
|
+
* status: 200,
|
|
254
|
+
* body: { updated: true }
|
|
255
|
+
* })
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
mockRequest(origin, options) {
|
|
259
|
+
const { path, method = 'GET', status = 200, body } = options;
|
|
260
|
+
return fetchMock
|
|
261
|
+
.get(origin)
|
|
262
|
+
.intercept({ path, method })
|
|
263
|
+
.reply(status, body ? JSON.stringify(body) : '');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Factory function to create a new FetchMock instance
|
|
268
|
+
*
|
|
269
|
+
* @returns A new FetchMock instance
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```typescript
|
|
273
|
+
* import { createFetchMock } from '@stratal/testing'
|
|
274
|
+
*
|
|
275
|
+
* const mock = createFetchMock()
|
|
276
|
+
*
|
|
277
|
+
* beforeEach(() => {
|
|
278
|
+
* mock.activate()
|
|
279
|
+
* mock.disableNetConnect()
|
|
280
|
+
* })
|
|
281
|
+
*
|
|
282
|
+
* afterEach(() => {
|
|
283
|
+
* mock.reset()
|
|
284
|
+
* })
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export function createFetchMock() {
|
|
288
|
+
return new FetchMock();
|
|
289
|
+
}
|
|
290
|
+
//# sourceMappingURL=fetch-mock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-mock.js","sourceRoot":"","sources":["../../../src/core/http/fetch-mock.ts"],"names":[],"mappings":"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"}
|