@velajs/testing 0.2.1 → 0.5.0
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 +5 -0
- package/dist/db/test-database.d.ts +27 -0
- package/dist/db/test-database.js +19 -0
- package/dist/http/path-utils.d.ts +11 -0
- package/dist/http/path-utils.js +37 -0
- package/dist/http/test-http-client.d.ts +37 -0
- package/dist/http/test-http-client.js +61 -0
- package/dist/http/test-http-request.d.ts +46 -0
- package/dist/http/test-http-request.js +87 -0
- package/dist/http/test-response.d.ts +76 -0
- package/dist/http/test-response.js +166 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +11 -0
- package/dist/sse/test-sse-connection.d.ts +46 -0
- package/dist/sse/test-sse-connection.js +196 -0
- package/dist/sse/test-sse-request.d.ts +30 -0
- package/dist/sse/test-sse-request.js +62 -0
- package/dist/testing-module.builder.js +19 -19
- package/dist/testing-module.d.ts +78 -1
- package/dist/testing-module.js +127 -4
- package/dist/websocket-node/index.d.ts +1 -0
- package/dist/websocket-node/index.js +109 -0
- package/dist/ws/test-ws-connection.d.ts +41 -0
- package/dist/ws/test-ws-connection.js +105 -0
- package/dist/ws/test-ws-request.d.ts +43 -0
- package/dist/ws/test-ws-request.js +69 -0
- package/package.json +32 -23
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (2026-07-04)
|
|
4
|
+
|
|
5
|
+
- `ComponentManager.init` call removed; provider overrides use the supported `Container.replaceProvider`; registers `DiscoveryService` mirroring bootstrap. Requires `@velajs/vela >=1.11.0`.
|
|
6
|
+
|
|
7
|
+
|
|
3
8
|
## 0.2.1 (2026-05-14)
|
|
4
9
|
|
|
5
10
|
### Fixes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDatabase
|
|
3
|
+
*
|
|
4
|
+
* A minimal driver contract for database assertions in tests. Vela has no ORM
|
|
5
|
+
* in core, so the harness ships only this interface plus thin assertion
|
|
6
|
+
* wrappers (`module.assertDatabaseHas/Missing/Count`). A consumer — or a future
|
|
7
|
+
* `@velajs/crud` driver — provides the concrete implementation; the harness
|
|
8
|
+
* never couples to a specific database library.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* class MyTestDb implements TestDatabase {
|
|
13
|
+
* async truncate() { ... }
|
|
14
|
+
* async has(table, where) { ... }
|
|
15
|
+
* async count(table) { ... }
|
|
16
|
+
* }
|
|
17
|
+
* await module.assertDatabaseHas(new MyTestDb(), 'user', { email: 'a@b.com' });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface TestDatabase {
|
|
21
|
+
/** Remove all rows from every table (reset between tests). */
|
|
22
|
+
truncate(): Promise<void>;
|
|
23
|
+
/** Whether a row matching `where` exists in `table`. */
|
|
24
|
+
has(table: string, where: Record<string, unknown>): Promise<boolean>;
|
|
25
|
+
/** The number of rows in `table`. */
|
|
26
|
+
count(table: string): Promise<number>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestDatabase
|
|
3
|
+
*
|
|
4
|
+
* A minimal driver contract for database assertions in tests. Vela has no ORM
|
|
5
|
+
* in core, so the harness ships only this interface plus thin assertion
|
|
6
|
+
* wrappers (`module.assertDatabaseHas/Missing/Count`). A consumer — or a future
|
|
7
|
+
* `@velajs/crud` driver — provides the concrete implementation; the harness
|
|
8
|
+
* never couples to a specific database library.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* class MyTestDb implements TestDatabase {
|
|
13
|
+
* async truncate() { ... }
|
|
14
|
+
* async has(table, where) { ... }
|
|
15
|
+
* async count(table) { ... }
|
|
16
|
+
* }
|
|
17
|
+
* await module.assertDatabaseHas(new MyTestDb(), 'user', { email: 'a@b.com' });
|
|
18
|
+
* ```
|
|
19
|
+
*/ export { };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the value at a dot-notation path (e.g. `data.user.id`).
|
|
3
|
+
* Returns `undefined` when any segment along the way is null/undefined.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getValueAtPath(obj: unknown, path: string): unknown;
|
|
6
|
+
/**
|
|
7
|
+
* Whether a dot-notation path exists on the object, even when the value at the
|
|
8
|
+
* path is `null`/`undefined`. Distinguishes "key present but null" from "key
|
|
9
|
+
* absent".
|
|
10
|
+
*/
|
|
11
|
+
export declare function hasValueAtPath(obj: unknown, path: string): boolean;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi).
|
|
2
|
+
/**
|
|
3
|
+
* Read the value at a dot-notation path (e.g. `data.user.id`).
|
|
4
|
+
* Returns `undefined` when any segment along the way is null/undefined.
|
|
5
|
+
*/ export function getValueAtPath(obj, path) {
|
|
6
|
+
const parts = path.split('.');
|
|
7
|
+
let current = obj;
|
|
8
|
+
for (const part of parts){
|
|
9
|
+
if (current === null || current === undefined) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
current = current[part];
|
|
13
|
+
}
|
|
14
|
+
return current;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Whether a dot-notation path exists on the object, even when the value at the
|
|
18
|
+
* path is `null`/`undefined`. Distinguishes "key present but null" from "key
|
|
19
|
+
* absent".
|
|
20
|
+
*/ export function hasValueAtPath(obj, path) {
|
|
21
|
+
const parts = path.split('.');
|
|
22
|
+
let current = obj;
|
|
23
|
+
for (const part of parts){
|
|
24
|
+
if (current === null || current === undefined) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (typeof current !== 'object') {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const record = current;
|
|
31
|
+
if (!(part in record)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
current = record[part];
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TestingModule } from '../testing-module.js';
|
|
2
|
+
import { TestHttpRequest } from './test-http-request.js';
|
|
3
|
+
/**
|
|
4
|
+
* TestHttpClient
|
|
5
|
+
*
|
|
6
|
+
* Fluent entry point for test HTTP requests. `forHost`/`withHeaders` return a
|
|
7
|
+
* new immutable client; the verb methods start a {@link TestHttpRequest}.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const res = await module.http
|
|
12
|
+
* .forHost('example.com')
|
|
13
|
+
* .post('/users')
|
|
14
|
+
* .withBody({ name: 'A' })
|
|
15
|
+
* .send();
|
|
16
|
+
* res.assertCreated();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class TestHttpClient {
|
|
20
|
+
private readonly module;
|
|
21
|
+
private readonly host;
|
|
22
|
+
private readonly defaultHeaders;
|
|
23
|
+
constructor(module: TestingModule, host?: string | null, defaultHeaders?: Headers);
|
|
24
|
+
/**
|
|
25
|
+
* Return a new client bound to `host`. Also sets the `Host` header so domain
|
|
26
|
+
* routing works even when the runtime reads the header rather than the URL.
|
|
27
|
+
*/
|
|
28
|
+
forHost(host: string): TestHttpClient;
|
|
29
|
+
/** Return a new client with additional default headers on every request. */
|
|
30
|
+
withHeaders(headers: Record<string, string>): TestHttpClient;
|
|
31
|
+
get(path: string): TestHttpRequest;
|
|
32
|
+
post(path: string): TestHttpRequest;
|
|
33
|
+
put(path: string): TestHttpRequest;
|
|
34
|
+
patch(path: string): TestHttpRequest;
|
|
35
|
+
delete(path: string): TestHttpRequest;
|
|
36
|
+
private createRequest;
|
|
37
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Stratal's i18n
|
|
2
|
+
// `withLocale` is dropped (vela i18n differs — optional follow-up).
|
|
3
|
+
import { TestHttpRequest } from "./test-http-request.js";
|
|
4
|
+
/**
|
|
5
|
+
* TestHttpClient
|
|
6
|
+
*
|
|
7
|
+
* Fluent entry point for test HTTP requests. `forHost`/`withHeaders` return a
|
|
8
|
+
* new immutable client; the verb methods start a {@link TestHttpRequest}.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const res = await module.http
|
|
13
|
+
* .forHost('example.com')
|
|
14
|
+
* .post('/users')
|
|
15
|
+
* .withBody({ name: 'A' })
|
|
16
|
+
* .send();
|
|
17
|
+
* res.assertCreated();
|
|
18
|
+
* ```
|
|
19
|
+
*/ export class TestHttpClient {
|
|
20
|
+
module;
|
|
21
|
+
host;
|
|
22
|
+
defaultHeaders;
|
|
23
|
+
constructor(module, host = null, defaultHeaders = new Headers()){
|
|
24
|
+
this.module = module;
|
|
25
|
+
this.host = host;
|
|
26
|
+
this.defaultHeaders = defaultHeaders;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Return a new client bound to `host`. Also sets the `Host` header so domain
|
|
30
|
+
* routing works even when the runtime reads the header rather than the URL.
|
|
31
|
+
*/ forHost(host) {
|
|
32
|
+
const headers = new Headers(this.defaultHeaders);
|
|
33
|
+
headers.set('Host', host);
|
|
34
|
+
return new TestHttpClient(this.module, host, headers);
|
|
35
|
+
}
|
|
36
|
+
/** Return a new client with additional default headers on every request. */ withHeaders(headers) {
|
|
37
|
+
const next = new Headers(this.defaultHeaders);
|
|
38
|
+
for (const [key, value] of Object.entries(headers)){
|
|
39
|
+
next.set(key, value);
|
|
40
|
+
}
|
|
41
|
+
return new TestHttpClient(this.module, this.host, next);
|
|
42
|
+
}
|
|
43
|
+
get(path) {
|
|
44
|
+
return this.createRequest('GET', path);
|
|
45
|
+
}
|
|
46
|
+
post(path) {
|
|
47
|
+
return this.createRequest('POST', path);
|
|
48
|
+
}
|
|
49
|
+
put(path) {
|
|
50
|
+
return this.createRequest('PUT', path);
|
|
51
|
+
}
|
|
52
|
+
patch(path) {
|
|
53
|
+
return this.createRequest('PATCH', path);
|
|
54
|
+
}
|
|
55
|
+
delete(path) {
|
|
56
|
+
return this.createRequest('DELETE', path);
|
|
57
|
+
}
|
|
58
|
+
createRequest(method, path) {
|
|
59
|
+
return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';
|
|
2
|
+
import { TestResponse } from './test-response.js';
|
|
3
|
+
/**
|
|
4
|
+
* TestHttpRequest
|
|
5
|
+
*
|
|
6
|
+
* Fluent builder for a single test HTTP request. `send()` builds a `Request`
|
|
7
|
+
* and drives it through `module.fetch()` (the full Hono pipeline).
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const res = await module.http
|
|
12
|
+
* .post('/users')
|
|
13
|
+
* .withBody({ name: 'A' })
|
|
14
|
+
* .withHeaders({ 'X-Trace': '1' })
|
|
15
|
+
* .send();
|
|
16
|
+
* res.assertCreated();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class TestHttpRequest {
|
|
20
|
+
private readonly method;
|
|
21
|
+
private readonly path;
|
|
22
|
+
private readonly module;
|
|
23
|
+
private readonly host;
|
|
24
|
+
private body;
|
|
25
|
+
private readonly requestHeaders;
|
|
26
|
+
private principal;
|
|
27
|
+
private resolver;
|
|
28
|
+
constructor(method: string, path: string, headers: Headers, module: TestingModule, host?: string | null);
|
|
29
|
+
/** Set the request body (JSON-serialized on send). */
|
|
30
|
+
withBody(data: unknown): this;
|
|
31
|
+
/** Merge additional headers. */
|
|
32
|
+
withHeaders(headers: Record<string, string>): this;
|
|
33
|
+
/** Set `Content-Type: application/json`. */
|
|
34
|
+
asJson(): this;
|
|
35
|
+
/**
|
|
36
|
+
* Authenticate the request as `principal`. The `resolver` (or a default one
|
|
37
|
+
* registered via `module.setAuthResolver`) turns the principal into request
|
|
38
|
+
* headers. The resolver signature `(module, principal) => Promise<Headers>`
|
|
39
|
+
* is the cross-package contract sibling packages (e.g. `@velajs/better-auth`)
|
|
40
|
+
* build against.
|
|
41
|
+
*/
|
|
42
|
+
actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this;
|
|
43
|
+
/** Build the `Request` and send it through `module.fetch()`. */
|
|
44
|
+
send(): Promise<TestResponse>;
|
|
45
|
+
private applyAuthentication;
|
|
46
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi): stratal's hard
|
|
2
|
+
// AuthService import is replaced by a generic auth-resolver seam so
|
|
3
|
+
// @velajs/testing stays free of optional-package dependencies.
|
|
4
|
+
import { TestResponse } from "./test-response.js";
|
|
5
|
+
/**
|
|
6
|
+
* TestHttpRequest
|
|
7
|
+
*
|
|
8
|
+
* Fluent builder for a single test HTTP request. `send()` builds a `Request`
|
|
9
|
+
* and drives it through `module.fetch()` (the full Hono pipeline).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* const res = await module.http
|
|
14
|
+
* .post('/users')
|
|
15
|
+
* .withBody({ name: 'A' })
|
|
16
|
+
* .withHeaders({ 'X-Trace': '1' })
|
|
17
|
+
* .send();
|
|
18
|
+
* res.assertCreated();
|
|
19
|
+
* ```
|
|
20
|
+
*/ export class TestHttpRequest {
|
|
21
|
+
method;
|
|
22
|
+
path;
|
|
23
|
+
module;
|
|
24
|
+
host;
|
|
25
|
+
body = undefined;
|
|
26
|
+
requestHeaders;
|
|
27
|
+
principal = null;
|
|
28
|
+
resolver = null;
|
|
29
|
+
constructor(method, path, headers, module, host = null){
|
|
30
|
+
this.method = method;
|
|
31
|
+
this.path = path;
|
|
32
|
+
this.module = module;
|
|
33
|
+
this.host = host;
|
|
34
|
+
this.requestHeaders = new Headers(headers);
|
|
35
|
+
}
|
|
36
|
+
/** Set the request body (JSON-serialized on send). */ withBody(data) {
|
|
37
|
+
this.body = data;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
/** Merge additional headers. */ withHeaders(headers) {
|
|
41
|
+
for (const [key, value] of Object.entries(headers)){
|
|
42
|
+
this.requestHeaders.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
/** Set `Content-Type: application/json`. */ asJson() {
|
|
47
|
+
this.requestHeaders.set('Content-Type', 'application/json');
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Authenticate the request as `principal`. The `resolver` (or a default one
|
|
52
|
+
* registered via `module.setAuthResolver`) turns the principal into request
|
|
53
|
+
* headers. The resolver signature `(module, principal) => Promise<Headers>`
|
|
54
|
+
* is the cross-package contract sibling packages (e.g. `@velajs/better-auth`)
|
|
55
|
+
* build against.
|
|
56
|
+
*/ actingAs(principal, resolver) {
|
|
57
|
+
this.principal = principal;
|
|
58
|
+
this.resolver = resolver ?? null;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
/** Build the `Request` and send it through `module.fetch()`. */ async send() {
|
|
62
|
+
await this.applyAuthentication();
|
|
63
|
+
const hasBody = this.body !== undefined && this.body !== null;
|
|
64
|
+
if (hasBody && !this.requestHeaders.has('Content-Type')) {
|
|
65
|
+
this.requestHeaders.set('Content-Type', 'application/json');
|
|
66
|
+
}
|
|
67
|
+
const url = new URL(this.path, `http://${this.host ?? 'localhost'}`);
|
|
68
|
+
const request = new Request(url.toString(), {
|
|
69
|
+
method: this.method,
|
|
70
|
+
headers: this.requestHeaders,
|
|
71
|
+
body: hasBody ? JSON.stringify(this.body) : null
|
|
72
|
+
});
|
|
73
|
+
const response = await this.module.fetch(request);
|
|
74
|
+
return new TestResponse(response);
|
|
75
|
+
}
|
|
76
|
+
async applyAuthentication() {
|
|
77
|
+
if (!this.principal) return;
|
|
78
|
+
const resolver = this.resolver ?? this.module.getAuthResolver();
|
|
79
|
+
if (!resolver) {
|
|
80
|
+
throw new Error('actingAs() requires an auth resolver. Pass one explicitly — ' + 'actingAs(principal, resolver) — or register a default with ' + 'module.setAuthResolver(resolver). For better-auth: ' + 'import { actingAs } from "@velajs/better-auth/testing".');
|
|
81
|
+
}
|
|
82
|
+
const headers = await resolver(this.module, this.principal);
|
|
83
|
+
for (const [key, value] of headers.entries()){
|
|
84
|
+
this.requestHeaders.set(key, value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TestResponse
|
|
3
|
+
*
|
|
4
|
+
* Wraps a `Response` with fluent, chainable assertions. Synchronous status /
|
|
5
|
+
* header assertions return `this`; JSON assertions (which must read the body)
|
|
6
|
+
* return `Promise<this>`.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const res = await module.http.get('/users/1').send();
|
|
11
|
+
* res.assertOk();
|
|
12
|
+
* await res.assertJsonPath('data.id', 1);
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare class TestResponse {
|
|
16
|
+
private readonly response;
|
|
17
|
+
private jsonData;
|
|
18
|
+
private textData;
|
|
19
|
+
constructor(response: Response);
|
|
20
|
+
/** The raw `Response`. */
|
|
21
|
+
get raw(): Response;
|
|
22
|
+
/** The response status code. */
|
|
23
|
+
get status(): number;
|
|
24
|
+
/** The response headers. */
|
|
25
|
+
get headers(): Headers;
|
|
26
|
+
/** Parse (and cache) the response body as JSON. */
|
|
27
|
+
json<T = unknown>(): Promise<T>;
|
|
28
|
+
/** Read (and cache) the response body as text. */
|
|
29
|
+
text(): Promise<string>;
|
|
30
|
+
/** Assert status is 200 OK. */
|
|
31
|
+
assertOk(): this;
|
|
32
|
+
/** Assert status is 201 Created. */
|
|
33
|
+
assertCreated(): this;
|
|
34
|
+
/** Assert status is 204 No Content. */
|
|
35
|
+
assertNoContent(): this;
|
|
36
|
+
/** Assert status is 400 Bad Request. */
|
|
37
|
+
assertBadRequest(): this;
|
|
38
|
+
/** Assert status is 401 Unauthorized. */
|
|
39
|
+
assertUnauthorized(): this;
|
|
40
|
+
/** Assert status is 403 Forbidden. */
|
|
41
|
+
assertForbidden(): this;
|
|
42
|
+
/** Assert status is 404 Not Found. */
|
|
43
|
+
assertNotFound(): this;
|
|
44
|
+
/** Assert status is 422 Unprocessable Entity. */
|
|
45
|
+
assertUnprocessable(): this;
|
|
46
|
+
/** Assert status is 500 Internal Server Error. */
|
|
47
|
+
assertServerError(): this;
|
|
48
|
+
/** Assert the response has the given status code. */
|
|
49
|
+
assertStatus(expected: number): this;
|
|
50
|
+
/** Assert the status is in the 2xx range. */
|
|
51
|
+
assertSuccessful(): this;
|
|
52
|
+
/** Assert each key in `expected` equals the corresponding top-level value. */
|
|
53
|
+
assertJson(expected: Record<string, unknown>): Promise<this>;
|
|
54
|
+
/** Assert the value at a dot-notation path equals `expected`. */
|
|
55
|
+
assertJsonPath(path: string, expected: unknown): Promise<this>;
|
|
56
|
+
/** Assert every path/value pair in `expectations` matches (batch assert). */
|
|
57
|
+
assertJsonPaths(expectations: Record<string, unknown>): Promise<this>;
|
|
58
|
+
/** Assert the top-level JSON object has every key in `structure`. */
|
|
59
|
+
assertJsonStructure(structure: string[]): Promise<this>;
|
|
60
|
+
/** Assert a path exists (value may be anything, including `null`). */
|
|
61
|
+
assertJsonPathExists(path: string): Promise<this>;
|
|
62
|
+
/** Assert a path does not exist. */
|
|
63
|
+
assertJsonPathMissing(path: string): Promise<this>;
|
|
64
|
+
/** Assert the value at a path satisfies a predicate. */
|
|
65
|
+
assertJsonPathMatches(path: string, matcher: (value: unknown) => boolean): Promise<this>;
|
|
66
|
+
/** Assert the string value at a path contains `substring`. */
|
|
67
|
+
assertJsonPathContains(path: string, substring: string): Promise<this>;
|
|
68
|
+
/** Assert the array value at a path includes `item`. */
|
|
69
|
+
assertJsonPathIncludes(path: string, item: unknown): Promise<this>;
|
|
70
|
+
/** Assert the array value at a path has `count` items. */
|
|
71
|
+
assertJsonPathCount(path: string, count: number): Promise<this>;
|
|
72
|
+
/** Assert a header is present, optionally equal to `expected`. */
|
|
73
|
+
assertHeader(name: string, expected?: string): this;
|
|
74
|
+
/** Assert a header is absent. */
|
|
75
|
+
assertHeaderMissing(name: string): this;
|
|
76
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
export { Test } from './test.js';
|
|
2
2
|
export { TestingModule } from './testing-module.js';
|
|
3
|
+
export type { ActingAsResolver, TestPrincipal } from './testing-module.js';
|
|
3
4
|
export { TestingModuleBuilder, OverrideBy } from './testing-module.builder.js';
|
|
5
|
+
export { TestHttpClient } from './http/test-http-client.js';
|
|
6
|
+
export { TestHttpRequest } from './http/test-http-request.js';
|
|
7
|
+
export { TestResponse } from './http/test-response.js';
|
|
8
|
+
export { getValueAtPath, hasValueAtPath } from './http/path-utils.js';
|
|
9
|
+
export { TestSseConnection } from './sse/test-sse-connection.js';
|
|
10
|
+
export type { TestSseEvent } from './sse/test-sse-connection.js';
|
|
11
|
+
export { TestSseRequest } from './sse/test-sse-request.js';
|
|
12
|
+
export { TestWsConnection } from './ws/test-ws-connection.js';
|
|
13
|
+
export { TestWsRequest, registerWsConnector, getWsConnector } from './ws/test-ws-request.js';
|
|
14
|
+
export type { WsConnector } from './ws/test-ws-request.js';
|
|
15
|
+
export type { TestDatabase } from './db/test-database.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
export { Test } from "./test.js";
|
|
2
2
|
export { TestingModule } from "./testing-module.js";
|
|
3
3
|
export { TestingModuleBuilder, OverrideBy } from "./testing-module.builder.js";
|
|
4
|
+
// HTTP
|
|
5
|
+
export { TestHttpClient } from "./http/test-http-client.js";
|
|
6
|
+
export { TestHttpRequest } from "./http/test-http-request.js";
|
|
7
|
+
export { TestResponse } from "./http/test-response.js";
|
|
8
|
+
export { getValueAtPath, hasValueAtPath } from "./http/path-utils.js";
|
|
9
|
+
// SSE
|
|
10
|
+
export { TestSseConnection } from "./sse/test-sse-connection.js";
|
|
11
|
+
export { TestSseRequest } from "./sse/test-sse-request.js";
|
|
12
|
+
// WebSocket (generic wrapper + pluggable transport seam)
|
|
13
|
+
export { TestWsConnection } from "./ws/test-ws-connection.js";
|
|
14
|
+
export { TestWsRequest, registerWsConnector, getWsConnector } from "./ws/test-ws-request.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|