@velajs/testing 0.5.0 → 0.5.1

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.
@@ -1,166 +0,0 @@
1
- // Ported from @stratal/testing (MIT, © Temitayo Fadojutimi), minus Macroable —
2
- // vela has no Macroable, so TestResponse is a plain class.
3
- import { expect } from "vitest";
4
- import { getValueAtPath, hasValueAtPath } from "./path-utils.js";
5
- /**
6
- * TestResponse
7
- *
8
- * Wraps a `Response` with fluent, chainable assertions. Synchronous status /
9
- * header assertions return `this`; JSON assertions (which must read the body)
10
- * return `Promise<this>`.
11
- *
12
- * @example
13
- * ```ts
14
- * const res = await module.http.get('/users/1').send();
15
- * res.assertOk();
16
- * await res.assertJsonPath('data.id', 1);
17
- * ```
18
- */ export class TestResponse {
19
- response;
20
- jsonData = null;
21
- textData = null;
22
- constructor(response){
23
- this.response = response;
24
- }
25
- /** The raw `Response`. */ get raw() {
26
- return this.response;
27
- }
28
- /** The response status code. */ get status() {
29
- return this.response.status;
30
- }
31
- /** The response headers. */ get headers() {
32
- return this.response.headers;
33
- }
34
- /** Parse (and cache) the response body as JSON. */ async json() {
35
- if (this.jsonData === null) {
36
- this.jsonData = await this.response.clone().json();
37
- }
38
- return this.jsonData;
39
- }
40
- /** Read (and cache) the response body as text. */ async text() {
41
- this.textData ??= await this.response.clone().text();
42
- return this.textData;
43
- }
44
- // ============================================================
45
- // Status assertions
46
- // ============================================================
47
- /** Assert status is 200 OK. */ assertOk() {
48
- return this.assertStatus(200);
49
- }
50
- /** Assert status is 201 Created. */ assertCreated() {
51
- return this.assertStatus(201);
52
- }
53
- /** Assert status is 204 No Content. */ assertNoContent() {
54
- return this.assertStatus(204);
55
- }
56
- /** Assert status is 400 Bad Request. */ assertBadRequest() {
57
- return this.assertStatus(400);
58
- }
59
- /** Assert status is 401 Unauthorized. */ assertUnauthorized() {
60
- return this.assertStatus(401);
61
- }
62
- /** Assert status is 403 Forbidden. */ assertForbidden() {
63
- return this.assertStatus(403);
64
- }
65
- /** Assert status is 404 Not Found. */ assertNotFound() {
66
- return this.assertStatus(404);
67
- }
68
- /** Assert status is 422 Unprocessable Entity. */ assertUnprocessable() {
69
- return this.assertStatus(422);
70
- }
71
- /** Assert status is 500 Internal Server Error. */ assertServerError() {
72
- return this.assertStatus(500);
73
- }
74
- /** Assert the response has the given status code. */ assertStatus(expected) {
75
- expect(this.response.status, `Expected status ${expected}, got ${this.response.status}`).toBe(expected);
76
- return this;
77
- }
78
- /** Assert the status is in the 2xx range. */ assertSuccessful() {
79
- expect(this.response.status >= 200 && this.response.status < 300, `Expected successful status (2xx), got ${this.response.status}`).toBe(true);
80
- return this;
81
- }
82
- // ============================================================
83
- // JSON assertions
84
- // ============================================================
85
- /** Assert each key in `expected` equals the corresponding top-level value. */ async assertJson(expected) {
86
- const actual = await this.json();
87
- for (const [key, value] of Object.entries(expected)){
88
- expect(actual[key], `Expected JSON key "${key}" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`).toStrictEqual(value);
89
- }
90
- return this;
91
- }
92
- /** Assert the value at a dot-notation path equals `expected`. */ async assertJsonPath(path, expected) {
93
- const json = await this.json();
94
- const actual = getValueAtPath(json, path);
95
- expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toStrictEqual(expected);
96
- return this;
97
- }
98
- /** Assert every path/value pair in `expectations` matches (batch assert). */ async assertJsonPaths(expectations) {
99
- const json = await this.json();
100
- for (const [path, expected] of Object.entries(expectations)){
101
- const actual = getValueAtPath(json, path);
102
- expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toStrictEqual(expected);
103
- }
104
- return this;
105
- }
106
- /** Assert the top-level JSON object has every key in `structure`. */ async assertJsonStructure(structure) {
107
- const json = await this.json();
108
- for (const key of structure){
109
- expect(key in json, `Expected JSON to have key "${key}", got keys: ${JSON.stringify(Object.keys(json))}`).toBe(true);
110
- }
111
- return this;
112
- }
113
- /** Assert a path exists (value may be anything, including `null`). */ async assertJsonPathExists(path) {
114
- const json = await this.json();
115
- expect(hasValueAtPath(json, path), `Expected JSON path "${path}" to exist`).toBe(true);
116
- return this;
117
- }
118
- /** Assert a path does not exist. */ async assertJsonPathMissing(path) {
119
- const json = await this.json();
120
- expect(hasValueAtPath(json, path), `Expected JSON path "${path}" to not exist`).toBe(false);
121
- return this;
122
- }
123
- /** Assert the value at a path satisfies a predicate. */ async assertJsonPathMatches(path, matcher) {
124
- const json = await this.json();
125
- const value = getValueAtPath(json, path);
126
- expect(matcher(value), `Expected JSON path "${path}" to match predicate, got ${JSON.stringify(value)}`).toBe(true);
127
- return this;
128
- }
129
- /** Assert the string value at a path contains `substring`. */ async assertJsonPathContains(path, substring) {
130
- const json = await this.json();
131
- const value = getValueAtPath(json, path);
132
- expect(typeof value === 'string', `Expected JSON path "${path}" to be a string, got ${typeof value}`).toBe(true);
133
- expect(value.includes(substring), `Expected JSON path "${path}" to contain "${substring}", got "${String(value)}"`).toBe(true);
134
- return this;
135
- }
136
- /** Assert the array value at a path includes `item`. */ async assertJsonPathIncludes(path, item) {
137
- const json = await this.json();
138
- const value = getValueAtPath(json, path);
139
- expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
140
- expect(value.includes(item), `Expected JSON path "${path}" to include ${JSON.stringify(item)}`).toBe(true);
141
- return this;
142
- }
143
- /** Assert the array value at a path has `count` items. */ async assertJsonPathCount(path, count) {
144
- const json = await this.json();
145
- const value = getValueAtPath(json, path);
146
- expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
147
- expect(value.length, `Expected JSON path "${path}" to have ${count} items, got ${value.length}`).toBe(count);
148
- return this;
149
- }
150
- // ============================================================
151
- // Header assertions
152
- // ============================================================
153
- /** Assert a header is present, optionally equal to `expected`. */ assertHeader(name, expected) {
154
- const actual = this.response.headers.get(name);
155
- expect(actual !== null, `Expected header "${name}" to be present`).toBe(true);
156
- if (expected !== undefined) {
157
- expect(actual, `Expected header "${name}" to be "${expected}", got "${actual}"`).toBe(expected);
158
- }
159
- return this;
160
- }
161
- /** Assert a header is absent. */ assertHeaderMissing(name) {
162
- const actual = this.response.headers.get(name);
163
- expect(actual, `Expected header "${name}" to be absent, but got "${actual}"`).toBeNull();
164
- return this;
165
- }
166
- }
@@ -1,46 +0,0 @@
1
- /** A parsed Server-Sent Event. */
2
- export interface TestSseEvent {
3
- data: string;
4
- event?: string;
5
- id?: string;
6
- retry?: number;
7
- }
8
- /**
9
- * TestSseConnection
10
- *
11
- * Reads a streaming `text/event-stream` response body and exposes queue-based
12
- * wait/assert helpers over the parsed events.
13
- *
14
- * @example
15
- * ```ts
16
- * const sse = await module.sse('/stream/events').connect();
17
- * await sse.assertEventData('ping');
18
- * await sse.waitForEnd();
19
- * ```
20
- */
21
- export declare class TestSseConnection {
22
- private readonly response;
23
- private readonly eventQueue;
24
- private eventWaiters;
25
- private streamEnded;
26
- private endWaiters;
27
- constructor(response: Response);
28
- /** The raw `Response`. */
29
- get raw(): Response;
30
- /** Wait for the next event (rejects after `timeout` ms). */
31
- waitForEvent(timeout?: number): Promise<TestSseEvent>;
32
- /** Wait for the stream to end (rejects after `timeout` ms). */
33
- waitForEnd(timeout?: number): Promise<void>;
34
- /** Collect all remaining events until the stream ends. */
35
- collectEvents(timeout?: number): Promise<TestSseEvent[]>;
36
- /** Assert the next event matches the expected partial shape. */
37
- assertEvent(expected: Partial<TestSseEvent>, timeout?: number): Promise<void>;
38
- /** Assert the next event's `data` equals `expected`. */
39
- assertEventData(expected: string, timeout?: number): Promise<void>;
40
- /** Assert the next event's `data` is JSON equal to `expected`. */
41
- assertJsonEventData<T>(expected: T, timeout?: number): Promise<void>;
42
- private startReading;
43
- private endStream;
44
- private parseEvent;
45
- private dispatchEvent;
46
- }
@@ -1,196 +0,0 @@
1
- // Ported near-verbatim from @stratal/testing (MIT, © Temitayo Fadojutimi).
2
- // Web-standard only (ReadableStream + TextDecoder), so it is edge-pure.
3
- import { expect } from "vitest";
4
- /**
5
- * TestSseConnection
6
- *
7
- * Reads a streaming `text/event-stream` response body and exposes queue-based
8
- * wait/assert helpers over the parsed events.
9
- *
10
- * @example
11
- * ```ts
12
- * const sse = await module.sse('/stream/events').connect();
13
- * await sse.assertEventData('ping');
14
- * await sse.waitForEnd();
15
- * ```
16
- */ export class TestSseConnection {
17
- response;
18
- eventQueue = [];
19
- eventWaiters = [];
20
- streamEnded = false;
21
- endWaiters = [];
22
- constructor(response){
23
- this.response = response;
24
- this.startReading();
25
- }
26
- /** The raw `Response`. */ get raw() {
27
- return this.response;
28
- }
29
- /** Wait for the next event (rejects after `timeout` ms). */ async waitForEvent(timeout = 5000) {
30
- if (this.eventQueue.length > 0) {
31
- return this.eventQueue.shift();
32
- }
33
- if (this.streamEnded) {
34
- throw new Error('SSE: stream has ended, no more events');
35
- }
36
- return new Promise((resolve, reject)=>{
37
- const waiter = (event)=>{
38
- clearTimeout(timer);
39
- resolve(event);
40
- };
41
- const timer = setTimeout(()=>{
42
- const index = this.eventWaiters.indexOf(waiter);
43
- if (index !== -1) this.eventWaiters.splice(index, 1);
44
- reject(new Error(`SSE: no event received within ${timeout}ms`));
45
- }, timeout);
46
- this.eventWaiters.push(waiter);
47
- });
48
- }
49
- /** Wait for the stream to end (rejects after `timeout` ms). */ async waitForEnd(timeout = 5000) {
50
- if (this.streamEnded) return;
51
- return new Promise((resolve, reject)=>{
52
- const waiter = ()=>{
53
- clearTimeout(timer);
54
- resolve();
55
- };
56
- const timer = setTimeout(()=>{
57
- const index = this.endWaiters.indexOf(waiter);
58
- if (index !== -1) this.endWaiters.splice(index, 1);
59
- reject(new Error(`SSE: stream did not end within ${timeout}ms`));
60
- }, timeout);
61
- this.endWaiters.push(waiter);
62
- });
63
- }
64
- /** Collect all remaining events until the stream ends. */ async collectEvents(timeout = 5000) {
65
- const events = [];
66
- if (this.streamEnded) {
67
- return [
68
- ...this.eventQueue.splice(0)
69
- ];
70
- }
71
- return new Promise((resolve, reject)=>{
72
- const originalDispatch = this.dispatchEvent.bind(this);
73
- this.dispatchEvent = (event)=>{
74
- events.push(event);
75
- originalDispatch(event);
76
- };
77
- const endWaiter = ()=>{
78
- clearTimeout(timer);
79
- this.dispatchEvent = originalDispatch;
80
- resolve(events);
81
- };
82
- const timer = setTimeout(()=>{
83
- this.dispatchEvent = originalDispatch;
84
- const index = this.endWaiters.indexOf(endWaiter);
85
- if (index !== -1) this.endWaiters.splice(index, 1);
86
- reject(new Error(`SSE: stream did not end within ${timeout}ms`));
87
- }, timeout);
88
- events.push(...this.eventQueue.splice(0));
89
- this.endWaiters.push(endWaiter);
90
- });
91
- }
92
- /** Assert the next event matches the expected partial shape. */ async assertEvent(expected, timeout = 5000) {
93
- const event = await this.waitForEvent(timeout);
94
- expect(event).toMatchObject(expected);
95
- }
96
- /** Assert the next event's `data` equals `expected`. */ async assertEventData(expected, timeout = 5000) {
97
- const event = await this.waitForEvent(timeout);
98
- expect(event.data, `Expected SSE data "${expected}", got "${event.data}"`).toBe(expected);
99
- }
100
- /** Assert the next event's `data` is JSON equal to `expected`. */ async assertJsonEventData(expected, timeout = 5000) {
101
- const event = await this.waitForEvent(timeout);
102
- const parsed = JSON.parse(event.data);
103
- expect(parsed).toEqual(expected);
104
- }
105
- startReading() {
106
- const body = this.response.body;
107
- if (!body) {
108
- this.streamEnded = true;
109
- return;
110
- }
111
- const reader = body.getReader();
112
- const decoder = new TextDecoder();
113
- let buffer = '';
114
- const read = async ()=>{
115
- try {
116
- for(;;){
117
- const { done, value } = await reader.read();
118
- if (done) {
119
- if (buffer.trim()) {
120
- const event = this.parseEvent(buffer);
121
- if (event) this.dispatchEvent(event);
122
- }
123
- this.endStream();
124
- return;
125
- }
126
- buffer += decoder.decode(value, {
127
- stream: true
128
- });
129
- const parts = buffer.split('\n\n');
130
- buffer = parts.pop();
131
- for (const part of parts){
132
- if (!part.trim()) continue;
133
- const event = this.parseEvent(part);
134
- if (event) this.dispatchEvent(event);
135
- }
136
- }
137
- } catch {
138
- this.endStream();
139
- }
140
- };
141
- void read();
142
- }
143
- endStream() {
144
- this.streamEnded = true;
145
- for (const waiter of this.endWaiters){
146
- waiter();
147
- }
148
- this.endWaiters = [];
149
- }
150
- parseEvent(raw) {
151
- const lines = raw.split('\n');
152
- const dataLines = [];
153
- let event;
154
- let id;
155
- let retry;
156
- for (const line of lines){
157
- if (line.startsWith(':')) continue; // comment line
158
- const colonIndex = line.indexOf(':');
159
- if (colonIndex === -1) continue;
160
- const field = line.slice(0, colonIndex);
161
- const value = line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1);
162
- switch(field){
163
- case 'data':
164
- dataLines.push(value);
165
- break;
166
- case 'event':
167
- event = value;
168
- break;
169
- case 'id':
170
- id = value;
171
- break;
172
- case 'retry':
173
- {
174
- const parsed = parseInt(value, 10);
175
- if (!Number.isNaN(parsed)) retry = parsed;
176
- break;
177
- }
178
- }
179
- }
180
- if (dataLines.length === 0) return null;
181
- const result = {
182
- data: dataLines.join('\n')
183
- };
184
- if (event !== undefined) result.event = event;
185
- if (id !== undefined) result.id = id;
186
- if (retry !== undefined) result.retry = retry;
187
- return result;
188
- }
189
- dispatchEvent(event) {
190
- if (this.eventWaiters.length > 0) {
191
- this.eventWaiters.shift()(event);
192
- } else {
193
- this.eventQueue.push(event);
194
- }
195
- }
196
- }
@@ -1,30 +0,0 @@
1
- import type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';
2
- import { TestSseConnection } from './test-sse-connection.js';
3
- /**
4
- * TestSseRequest
5
- *
6
- * Builder for a Server-Sent Events connection. `connect()` issues a GET through
7
- * `module.fetch()`, asserts a `text/event-stream` 200, and wraps the streaming
8
- * body in a {@link TestSseConnection}.
9
- *
10
- * @example
11
- * ```ts
12
- * const sse = await module.sse('/stream/events').connect();
13
- * await sse.assertEvent({ event: 'message', data: 'hello' });
14
- * ```
15
- */
16
- export declare class TestSseRequest {
17
- private readonly path;
18
- private readonly module;
19
- private readonly requestHeaders;
20
- private principal;
21
- private resolver;
22
- constructor(path: string, module: TestingModule);
23
- /** Merge additional headers onto the SSE request. */
24
- withHeaders(headers: Record<string, string>): this;
25
- /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */
26
- actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this;
27
- /** Open the stream and return a live {@link TestSseConnection}. */
28
- connect(): Promise<TestSseConnection>;
29
- private applyAuthentication;
30
- }
@@ -1,62 +0,0 @@
1
- // Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Auth uses the
2
- // generic resolver seam instead of a hard AuthService import.
3
- import { expect } from "vitest";
4
- import { TestSseConnection } from "./test-sse-connection.js";
5
- /**
6
- * TestSseRequest
7
- *
8
- * Builder for a Server-Sent Events connection. `connect()` issues a GET through
9
- * `module.fetch()`, asserts a `text/event-stream` 200, and wraps the streaming
10
- * body in a {@link TestSseConnection}.
11
- *
12
- * @example
13
- * ```ts
14
- * const sse = await module.sse('/stream/events').connect();
15
- * await sse.assertEvent({ event: 'message', data: 'hello' });
16
- * ```
17
- */ export class TestSseRequest {
18
- path;
19
- module;
20
- requestHeaders = new Headers();
21
- principal = null;
22
- resolver = null;
23
- constructor(path, module){
24
- this.path = path;
25
- this.module = module;
26
- }
27
- /** Merge additional headers onto the SSE request. */ withHeaders(headers) {
28
- for (const [key, value] of Object.entries(headers)){
29
- this.requestHeaders.set(key, value);
30
- }
31
- return this;
32
- }
33
- /** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */ actingAs(principal, resolver) {
34
- this.principal = principal;
35
- this.resolver = resolver ?? null;
36
- return this;
37
- }
38
- /** Open the stream and return a live {@link TestSseConnection}. */ async connect() {
39
- await this.applyAuthentication();
40
- this.requestHeaders.set('Accept', 'text/event-stream');
41
- const url = new URL(this.path, 'http://localhost');
42
- const request = new Request(url.toString(), {
43
- headers: this.requestHeaders
44
- });
45
- const response = await this.module.fetch(request);
46
- expect(response.status, `Expected status 200, got ${response.status}`).toBe(200);
47
- const contentType = response.headers.get('content-type') ?? '';
48
- expect(contentType.includes('text/event-stream'), `Expected content-type "text/event-stream", got "${contentType}"`).toBe(true);
49
- return new TestSseConnection(response);
50
- }
51
- async applyAuthentication() {
52
- if (!this.principal) return;
53
- const resolver = this.resolver ?? this.module.getAuthResolver();
54
- if (!resolver) {
55
- throw new Error('actingAs() requires an auth resolver. Pass one explicitly or register ' + 'a default with module.setAuthResolver(resolver).');
56
- }
57
- const headers = await resolver(this.module, this.principal);
58
- for (const [key, value] of headers.entries()){
59
- this.requestHeaders.set(key, value);
60
- }
61
- }
62
- }
package/dist/test.d.ts DELETED
@@ -1,5 +0,0 @@
1
- import type { ModuleOptions } from '@velajs/vela';
2
- import { TestingModuleBuilder } from './testing-module.builder.js';
3
- export declare const Test: {
4
- createTestingModule(metadata: ModuleOptions): TestingModuleBuilder;
5
- };
package/dist/test.js DELETED
@@ -1,6 +0,0 @@
1
- import { TestingModuleBuilder } from "./testing-module.builder.js";
2
- export const Test = {
3
- createTestingModule (metadata) {
4
- return new TestingModuleBuilder(metadata);
5
- }
6
- };
@@ -1,25 +0,0 @@
1
- import { type ModuleOptions, type Token, type Type } from '@velajs/vela';
2
- import { TestingModule } from './testing-module.js';
3
- export declare class OverrideBy {
4
- private readonly builder;
5
- private readonly token;
6
- constructor(builder: TestingModuleBuilder, token: Token);
7
- useValue(value: unknown): TestingModuleBuilder;
8
- useClass(cls: Type): TestingModuleBuilder;
9
- useFactory(options: {
10
- factory: (...args: unknown[]) => unknown;
11
- inject?: Token[];
12
- }): TestingModuleBuilder;
13
- }
14
- export declare class TestingModuleBuilder {
15
- private readonly metadata;
16
- private overrides;
17
- constructor(metadata: ModuleOptions);
18
- overrideProvider(token: Token): OverrideBy;
19
- overrideGuard(guard: Type): OverrideBy;
20
- overridePipe(pipe: Type): OverrideBy;
21
- overrideInterceptor(interceptor: Type): OverrideBy;
22
- overrideFilter(filter: Type): OverrideBy;
23
- private addOverride;
24
- compile(): Promise<TestingModule>;
25
- }