@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.
- package/CHANGELOG.md +7 -2
- package/dist/index.d.ts +484 -15
- package/dist/index.js +866 -14
- package/dist/index.js.map +1 -0
- package/dist/test-ws-connection-BHlEKwQz.js +166 -0
- package/dist/test-ws-connection-BHlEKwQz.js.map +1 -0
- package/dist/websocket-node/index.d.ts +1 -1
- package/dist/websocket-node/index.js +67 -102
- package/dist/websocket-node/index.js.map +1 -0
- package/package.json +59 -45
- package/dist/db/test-database.d.ts +0 -27
- package/dist/db/test-database.js +0 -19
- package/dist/http/path-utils.d.ts +0 -11
- package/dist/http/path-utils.js +0 -37
- package/dist/http/test-http-client.d.ts +0 -37
- package/dist/http/test-http-client.js +0 -61
- package/dist/http/test-http-request.d.ts +0 -46
- package/dist/http/test-http-request.js +0 -87
- package/dist/http/test-response.d.ts +0 -76
- package/dist/http/test-response.js +0 -166
- package/dist/sse/test-sse-connection.d.ts +0 -46
- package/dist/sse/test-sse-connection.js +0 -196
- package/dist/sse/test-sse-request.d.ts +0 -30
- package/dist/sse/test-sse-request.js +0 -62
- package/dist/test.d.ts +0 -5
- package/dist/test.js +0 -6
- package/dist/testing-module.builder.d.ts +0 -25
- package/dist/testing-module.builder.js +0 -160
- package/dist/testing-module.d.ts +0 -87
- package/dist/testing-module.js +0 -141
- package/dist/ws/test-ws-connection.d.ts +0 -41
- package/dist/ws/test-ws-connection.js +0 -105
- package/dist/ws/test-ws-request.d.ts +0 -43
- package/dist/ws/test-ws-request.js +0 -69
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { DiscoveryService, REQUEST_CONTEXT, Scope } from "@velajs/vela";
|
|
2
|
-
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE, Container, MetadataRegistry, ModuleLoader, ModuleRef, RouteManager, VelaApplication, bindAppProviders } from "@velajs/vela/internal";
|
|
3
|
-
import { TestingModule } from "./testing-module.js";
|
|
4
|
-
export class OverrideBy {
|
|
5
|
-
builder;
|
|
6
|
-
token;
|
|
7
|
-
constructor(builder, token){
|
|
8
|
-
this.builder = builder;
|
|
9
|
-
this.token = token;
|
|
10
|
-
}
|
|
11
|
-
useValue(value) {
|
|
12
|
-
this.builder['addOverride']({
|
|
13
|
-
token: this.token,
|
|
14
|
-
provider: {
|
|
15
|
-
provide: this.token,
|
|
16
|
-
useValue: value
|
|
17
|
-
}
|
|
18
|
-
});
|
|
19
|
-
return this.builder;
|
|
20
|
-
}
|
|
21
|
-
useClass(cls) {
|
|
22
|
-
this.builder['addOverride']({
|
|
23
|
-
token: this.token,
|
|
24
|
-
provider: {
|
|
25
|
-
provide: this.token,
|
|
26
|
-
useClass: cls
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
return this.builder;
|
|
30
|
-
}
|
|
31
|
-
useFactory(options) {
|
|
32
|
-
this.builder['addOverride']({
|
|
33
|
-
token: this.token,
|
|
34
|
-
provider: {
|
|
35
|
-
provide: this.token,
|
|
36
|
-
useFactory: options.factory,
|
|
37
|
-
inject: options.inject
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
return this.builder;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export class TestingModuleBuilder {
|
|
44
|
-
metadata;
|
|
45
|
-
overrides = [];
|
|
46
|
-
constructor(metadata){
|
|
47
|
-
this.metadata = metadata;
|
|
48
|
-
}
|
|
49
|
-
overrideProvider(token) {
|
|
50
|
-
return new OverrideBy(this, token);
|
|
51
|
-
}
|
|
52
|
-
overrideGuard(guard) {
|
|
53
|
-
return new OverrideBy(this, guard);
|
|
54
|
-
}
|
|
55
|
-
overridePipe(pipe) {
|
|
56
|
-
return new OverrideBy(this, pipe);
|
|
57
|
-
}
|
|
58
|
-
overrideInterceptor(interceptor) {
|
|
59
|
-
return new OverrideBy(this, interceptor);
|
|
60
|
-
}
|
|
61
|
-
overrideFilter(filter) {
|
|
62
|
-
return new OverrideBy(this, filter);
|
|
63
|
-
}
|
|
64
|
-
addOverride(entry) {
|
|
65
|
-
const idx = this.overrides.findIndex((o)=>o.token === entry.token);
|
|
66
|
-
if (idx !== -1) {
|
|
67
|
-
this.overrides[idx] = entry;
|
|
68
|
-
} else {
|
|
69
|
-
this.overrides.push(entry);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
async compile() {
|
|
73
|
-
let TestRootModule = class TestRootModule {
|
|
74
|
-
};
|
|
75
|
-
MetadataRegistry.setModuleOptions(TestRootModule, {
|
|
76
|
-
imports: this.metadata.imports,
|
|
77
|
-
providers: this.metadata.providers,
|
|
78
|
-
controllers: this.metadata.controllers,
|
|
79
|
-
exports: this.metadata.exports
|
|
80
|
-
});
|
|
81
|
-
const container = new Container();
|
|
82
|
-
// Replicate bootstrap()'s global token setup so request-pipeline tests
|
|
83
|
-
// can resolve REQUEST_CONTEXT, ModuleRef, and the APP_* sentinel tokens
|
|
84
|
-
// from any module scope. Without this, guards / decorators that inject
|
|
85
|
-
// REQUEST_CONTEXT through ExecutionContext fail with "no provider"
|
|
86
|
-
// when the request enters the pipeline. See vela/src/factory/bootstrap.ts.
|
|
87
|
-
container.register({
|
|
88
|
-
provide: Container,
|
|
89
|
-
useValue: container
|
|
90
|
-
});
|
|
91
|
-
container.markGlobalToken(Container);
|
|
92
|
-
container.register({
|
|
93
|
-
provide: ModuleRef,
|
|
94
|
-
useFactory: (c)=>new ModuleRef(c),
|
|
95
|
-
inject: [
|
|
96
|
-
Container
|
|
97
|
-
]
|
|
98
|
-
});
|
|
99
|
-
container.markGlobalToken(ModuleRef);
|
|
100
|
-
// Decorator-driven discovery — global so any provider can inject it, and so
|
|
101
|
-
// callOnApplicationBootstrap() reuses this instance instead of self-building
|
|
102
|
-
// one. Mirrors vela/src/factory/bootstrap.ts.
|
|
103
|
-
container.register({
|
|
104
|
-
provide: DiscoveryService,
|
|
105
|
-
useFactory: (c)=>new DiscoveryService(c),
|
|
106
|
-
inject: [
|
|
107
|
-
Container
|
|
108
|
-
]
|
|
109
|
-
});
|
|
110
|
-
container.markGlobalToken(DiscoveryService);
|
|
111
|
-
for (const t of [
|
|
112
|
-
APP_GUARD,
|
|
113
|
-
APP_PIPE,
|
|
114
|
-
APP_INTERCEPTOR,
|
|
115
|
-
APP_FILTER,
|
|
116
|
-
APP_MIDDLEWARE
|
|
117
|
-
]){
|
|
118
|
-
container.markGlobalToken(t);
|
|
119
|
-
}
|
|
120
|
-
// REQUEST_CONTEXT is seeded into each per-request child container by
|
|
121
|
-
// RouteManager via setRequestInstance. Registering with a throw-factory
|
|
122
|
-
// here ensures findRegistration succeeds (so the child's cached request
|
|
123
|
-
// instance is returned) while surfacing a clear error if the token is
|
|
124
|
-
// ever resolved outside the request path.
|
|
125
|
-
container.register({
|
|
126
|
-
provide: REQUEST_CONTEXT,
|
|
127
|
-
scope: Scope.REQUEST,
|
|
128
|
-
useFactory: ()=>{
|
|
129
|
-
throw new Error('REQUEST_CONTEXT can only be resolved inside a request — ' + 'it is seeded by RouteManager when the request enters the pipeline.');
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
container.markGlobalToken(REQUEST_CONTEXT);
|
|
133
|
-
// Pre-register at root so `moduleRef.get(token)` and framework-internal
|
|
134
|
-
// resolves (instantiate(guard, container) with no requestingModuleId) see
|
|
135
|
-
// the override immediately.
|
|
136
|
-
for (const override of this.overrides){
|
|
137
|
-
container.register(override.provider);
|
|
138
|
-
}
|
|
139
|
-
const routeManager = new RouteManager(container);
|
|
140
|
-
const loader = new ModuleLoader(container, routeManager);
|
|
141
|
-
loader.load(TestRootModule);
|
|
142
|
-
// Force-apply overrides into every module bucket that already holds the
|
|
143
|
-
// token (plus root). Without this, controller constructor-injection (which
|
|
144
|
-
// passes requestingModuleId to findRegistration) finds the module's own
|
|
145
|
-
// registration first and never consults the root override. The default
|
|
146
|
-
// 'all-existing' buckets replace every non-root bucket holding the token
|
|
147
|
-
// and re-register at root — the supported form of the old private loop.
|
|
148
|
-
for (const override of this.overrides){
|
|
149
|
-
container.replaceProvider(override.provider);
|
|
150
|
-
}
|
|
151
|
-
bindAppProviders(routeManager, container, loader);
|
|
152
|
-
const app = new VelaApplication(container, routeManager);
|
|
153
|
-
const instances = await loader.resolveAllInstances();
|
|
154
|
-
app.setInstances(instances);
|
|
155
|
-
await app.callOnModuleInit();
|
|
156
|
-
await app.callOnApplicationBootstrap();
|
|
157
|
-
await app.initRoutes();
|
|
158
|
-
return new TestingModule(app, container);
|
|
159
|
-
}
|
|
160
|
-
}
|
package/dist/testing-module.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { type Token, type Type, type VelaApplication } from '@velajs/vela';
|
|
2
|
-
import type { Container } from '@velajs/vela/internal';
|
|
3
|
-
import { type ISeeder } from '@velajs/vela/seeder';
|
|
4
|
-
import type { TestDatabase } from './db/test-database.js';
|
|
5
|
-
import { TestHttpClient } from './http/test-http-client.js';
|
|
6
|
-
import { TestSseRequest } from './sse/test-sse-request.js';
|
|
7
|
-
import { TestWsRequest } from './ws/test-ws-request.js';
|
|
8
|
-
/** A test principal — an opaque object the auth resolver turns into headers. */
|
|
9
|
-
export type TestPrincipal = Record<string, unknown>;
|
|
10
|
-
/**
|
|
11
|
-
* Turns a principal into request headers (session cookie, bearer token, …).
|
|
12
|
-
* The signature `(module, principal) => Promise<Headers>` is a cross-package
|
|
13
|
-
* contract: sibling packages (e.g. `@velajs/better-auth/testing`) build a
|
|
14
|
-
* resolver against it. Kept generic so `@velajs/testing` needs no auth deps.
|
|
15
|
-
*/
|
|
16
|
-
export type ActingAsResolver = (module: TestingModule, principal: TestPrincipal) => Promise<Headers>;
|
|
17
|
-
/**
|
|
18
|
-
* TestingModule
|
|
19
|
-
*
|
|
20
|
-
* The compiled test harness. Beyond `get`/`createApplication`/`close`, it adds
|
|
21
|
-
* Laravel-flavored ergonomics: a fluent HTTP client, SSE/WS builders, request-
|
|
22
|
-
* scope execution, seeding, and database assertion wrappers.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
|
|
27
|
-
* await module.http.post('/users').withBody({ name: 'A' }).send()
|
|
28
|
-
* .then((r) => r.assertCreated());
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export declare class TestingModule {
|
|
32
|
-
private readonly app;
|
|
33
|
-
private readonly container;
|
|
34
|
-
private _http;
|
|
35
|
-
private honoApp;
|
|
36
|
-
private authResolver;
|
|
37
|
-
constructor(app: VelaApplication, container: Container);
|
|
38
|
-
/** Resolve a provider from the root container. */
|
|
39
|
-
get<T>(token: Token<T>): T;
|
|
40
|
-
/** Build (once) and return the underlying application. */
|
|
41
|
-
createApplication(): Promise<VelaApplication>;
|
|
42
|
-
/** Lazy fluent HTTP client bound to this module. */
|
|
43
|
-
get http(): TestHttpClient;
|
|
44
|
-
/** Start an SSE connection builder for `path`. */
|
|
45
|
-
sse(path: string): TestSseRequest;
|
|
46
|
-
/** Start a WebSocket connection builder for `path` (needs a transport adapter). */
|
|
47
|
-
ws(path: string): TestWsRequest;
|
|
48
|
-
/**
|
|
49
|
-
* Drive a `Request` through the full Hono pipeline. The Hono app is built
|
|
50
|
-
* once and reused across requests.
|
|
51
|
-
*/
|
|
52
|
-
fetch(request: Request, env?: unknown, ctx?: unknown): Promise<Response>;
|
|
53
|
-
/**
|
|
54
|
-
* Register a default auth resolver used by `actingAs(principal)` when no
|
|
55
|
-
* resolver is passed explicitly.
|
|
56
|
-
*/
|
|
57
|
-
setAuthResolver(resolver: ActingAsResolver): this;
|
|
58
|
-
/** The default auth resolver, if one was registered. */
|
|
59
|
-
getAuthResolver(): ActingAsResolver | null;
|
|
60
|
-
/**
|
|
61
|
-
* Run `callback` inside a request-scoped child container seeded with a mock
|
|
62
|
-
* {@link RequestContext}, so REQUEST-scoped providers (and anything injecting
|
|
63
|
-
* `REQUEST_CONTEXT`) resolve. The child is disposed afterwards.
|
|
64
|
-
*/
|
|
65
|
-
runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T>;
|
|
66
|
-
/**
|
|
67
|
-
* Run the given `@Seeder()` classes, each in its own request scope. Throws if
|
|
68
|
-
* a class is not a registered seeder. Requires `SeederModule` (or the seeders
|
|
69
|
-
* themselves) to be present in the module graph.
|
|
70
|
-
*/
|
|
71
|
-
seed(...SeederClasses: Type<ISeeder>[]): Promise<void>;
|
|
72
|
-
/** Assert a row matching `where` exists in `table` (via a {@link TestDatabase}). */
|
|
73
|
-
assertDatabaseHas(db: TestDatabase, table: string, where: Record<string, unknown>): Promise<void>;
|
|
74
|
-
/** Assert no row matching `where` exists in `table`. */
|
|
75
|
-
assertDatabaseMissing(db: TestDatabase, table: string, where: Record<string, unknown>): Promise<void>;
|
|
76
|
-
/** Assert `table` has exactly `expected` rows. */
|
|
77
|
-
assertDatabaseCount(db: TestDatabase, table: string, expected: number): Promise<void>;
|
|
78
|
-
/** Dispose the application. */
|
|
79
|
-
close(signal?: string): Promise<void>;
|
|
80
|
-
private ensureHono;
|
|
81
|
-
/**
|
|
82
|
-
* Build a minimal, functional {@link RequestContext} for out-of-band request
|
|
83
|
-
* scopes. Vela has no `createMockRouterContext`; a real (empty) Hono `Context`
|
|
84
|
-
* backs the `hono` field so nothing dangles.
|
|
85
|
-
*/
|
|
86
|
-
private createMockRequestContext;
|
|
87
|
-
}
|
package/dist/testing-module.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { Context } from "hono";
|
|
2
|
-
import { REQUEST_CONTEXT } from "@velajs/vela";
|
|
3
|
-
import { SeederRegistry } from "@velajs/vela/seeder";
|
|
4
|
-
import { expect } from "vitest";
|
|
5
|
-
import { TestHttpClient } from "./http/test-http-client.js";
|
|
6
|
-
import { TestSseRequest } from "./sse/test-sse-request.js";
|
|
7
|
-
import { TestWsRequest } from "./ws/test-ws-request.js";
|
|
8
|
-
/**
|
|
9
|
-
* TestingModule
|
|
10
|
-
*
|
|
11
|
-
* The compiled test harness. Beyond `get`/`createApplication`/`close`, it adds
|
|
12
|
-
* Laravel-flavored ergonomics: a fluent HTTP client, SSE/WS builders, request-
|
|
13
|
-
* scope execution, seeding, and database assertion wrappers.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
|
|
18
|
-
* await module.http.post('/users').withBody({ name: 'A' }).send()
|
|
19
|
-
* .then((r) => r.assertCreated());
|
|
20
|
-
* ```
|
|
21
|
-
*/ export class TestingModule {
|
|
22
|
-
app;
|
|
23
|
-
container;
|
|
24
|
-
_http = null;
|
|
25
|
-
honoApp = null;
|
|
26
|
-
authResolver = null;
|
|
27
|
-
constructor(app, container){
|
|
28
|
-
this.app = app;
|
|
29
|
-
this.container = container;
|
|
30
|
-
}
|
|
31
|
-
/** Resolve a provider from the root container. */ get(token) {
|
|
32
|
-
return this.container.resolve(token);
|
|
33
|
-
}
|
|
34
|
-
/** Build (once) and return the underlying application. */ async createApplication() {
|
|
35
|
-
await this.app.initRoutes();
|
|
36
|
-
return this.app;
|
|
37
|
-
}
|
|
38
|
-
/** Lazy fluent HTTP client bound to this module. */ get http() {
|
|
39
|
-
this._http ??= new TestHttpClient(this);
|
|
40
|
-
return this._http;
|
|
41
|
-
}
|
|
42
|
-
/** Start an SSE connection builder for `path`. */ sse(path) {
|
|
43
|
-
return new TestSseRequest(path, this);
|
|
44
|
-
}
|
|
45
|
-
/** Start a WebSocket connection builder for `path` (needs a transport adapter). */ ws(path) {
|
|
46
|
-
return new TestWsRequest(path, this);
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Drive a `Request` through the full Hono pipeline. The Hono app is built
|
|
50
|
-
* once and reused across requests.
|
|
51
|
-
*/ async fetch(request, env, ctx) {
|
|
52
|
-
const hono = await this.ensureHono();
|
|
53
|
-
return hono.fetch(request, env, ctx);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Register a default auth resolver used by `actingAs(principal)` when no
|
|
57
|
-
* resolver is passed explicitly.
|
|
58
|
-
*/ setAuthResolver(resolver) {
|
|
59
|
-
this.authResolver = resolver;
|
|
60
|
-
return this;
|
|
61
|
-
}
|
|
62
|
-
/** The default auth resolver, if one was registered. */ getAuthResolver() {
|
|
63
|
-
return this.authResolver;
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Run `callback` inside a request-scoped child container seeded with a mock
|
|
67
|
-
* {@link RequestContext}, so REQUEST-scoped providers (and anything injecting
|
|
68
|
-
* `REQUEST_CONTEXT`) resolve. The child is disposed afterwards.
|
|
69
|
-
*/ async runInRequestScope(callback) {
|
|
70
|
-
const child = this.container.createChild();
|
|
71
|
-
child.setRequestInstance(REQUEST_CONTEXT, this.createMockRequestContext());
|
|
72
|
-
try {
|
|
73
|
-
return await callback(child);
|
|
74
|
-
} finally{
|
|
75
|
-
await child.dispose();
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Run the given `@Seeder()` classes, each in its own request scope. Throws if
|
|
80
|
-
* a class is not a registered seeder. Requires `SeederModule` (or the seeders
|
|
81
|
-
* themselves) to be present in the module graph.
|
|
82
|
-
*/ async seed(...SeederClasses) {
|
|
83
|
-
const registry = this.container.resolve(SeederRegistry);
|
|
84
|
-
const known = new Set(registry.list().map((s)=>s.target));
|
|
85
|
-
for (const SeederClass of SeederClasses){
|
|
86
|
-
if (!known.has(SeederClass)) {
|
|
87
|
-
throw new Error(`Seeder "${SeederClass.name}" is not registered. Add it to a module's ` + 'providers or SeederModule.forRoot({ seeders: [...] }).');
|
|
88
|
-
}
|
|
89
|
-
await this.runInRequestScope(async (child)=>{
|
|
90
|
-
const instance = child.resolve(SeederClass);
|
|
91
|
-
await instance.run();
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/** Assert a row matching `where` exists in `table` (via a {@link TestDatabase}). */ async assertDatabaseHas(db, table, where) {
|
|
96
|
-
const exists = await db.has(table, where);
|
|
97
|
-
expect(exists, `Expected ${table} to have a row matching ${JSON.stringify(where)}`).toBe(true);
|
|
98
|
-
}
|
|
99
|
-
/** Assert no row matching `where` exists in `table`. */ async assertDatabaseMissing(db, table, where) {
|
|
100
|
-
const exists = await db.has(table, where);
|
|
101
|
-
expect(exists, `Expected ${table} NOT to have a row matching ${JSON.stringify(where)}`).toBe(false);
|
|
102
|
-
}
|
|
103
|
-
/** Assert `table` has exactly `expected` rows. */ async assertDatabaseCount(db, table, expected) {
|
|
104
|
-
const actual = await db.count(table);
|
|
105
|
-
expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected);
|
|
106
|
-
}
|
|
107
|
-
/** Dispose the application. */ async close(signal) {
|
|
108
|
-
await this.app.close(signal);
|
|
109
|
-
}
|
|
110
|
-
async ensureHono() {
|
|
111
|
-
if (!this.honoApp) {
|
|
112
|
-
const app = await this.createApplication();
|
|
113
|
-
this.honoApp = app.getHonoApp();
|
|
114
|
-
}
|
|
115
|
-
return this.honoApp;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Build a minimal, functional {@link RequestContext} for out-of-band request
|
|
119
|
-
* scopes. Vela has no `createMockRouterContext`; a real (empty) Hono `Context`
|
|
120
|
-
* backs the `hono` field so nothing dangles.
|
|
121
|
-
*/ createMockRequestContext() {
|
|
122
|
-
const bag = new Map();
|
|
123
|
-
const request = new Request('http://localhost/');
|
|
124
|
-
const hono = new Context(request);
|
|
125
|
-
return {
|
|
126
|
-
id: crypto.randomUUID(),
|
|
127
|
-
receivedAt: new Date(),
|
|
128
|
-
request,
|
|
129
|
-
hono,
|
|
130
|
-
set (key, value) {
|
|
131
|
-
bag.set(key, value);
|
|
132
|
-
},
|
|
133
|
-
get (key) {
|
|
134
|
-
return bag.get(key);
|
|
135
|
-
},
|
|
136
|
-
has (key) {
|
|
137
|
-
return bag.has(key);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TestWsConnection
|
|
3
|
-
*
|
|
4
|
-
* Wraps a live `WebSocket` with queue-based wait/assert helpers. Transport is
|
|
5
|
-
* supplied by the caller — the core harness never opens a socket itself (see
|
|
6
|
-
* `@velajs/testing/websocket-node`).
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* const ws = await module.ws('/ws/chat').connect();
|
|
11
|
-
* ws.send('hi');
|
|
12
|
-
* await ws.assertMessage('echo:hi');
|
|
13
|
-
* ws.close();
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
export declare class TestWsConnection {
|
|
17
|
-
private readonly ws;
|
|
18
|
-
private readonly cleanup?;
|
|
19
|
-
private readonly messageQueue;
|
|
20
|
-
private messageWaiters;
|
|
21
|
-
private closeEvent;
|
|
22
|
-
private closeWaiters;
|
|
23
|
-
constructor(ws: WebSocket, cleanup?: (() => void | Promise<void>) | undefined);
|
|
24
|
-
/** The raw `WebSocket`. */
|
|
25
|
-
get raw(): WebSocket;
|
|
26
|
-
/** Send a message. */
|
|
27
|
-
send(data: string | ArrayBufferLike | ArrayBufferView): void;
|
|
28
|
-
/** Close the connection. */
|
|
29
|
-
close(code?: number, reason?: string): void;
|
|
30
|
-
/** Wait for the next message (rejects after `timeout` ms). */
|
|
31
|
-
waitForMessage(timeout?: number): Promise<string | ArrayBuffer>;
|
|
32
|
-
/** Wait for the connection to close (rejects after `timeout` ms). */
|
|
33
|
-
waitForClose(timeout?: number): Promise<{
|
|
34
|
-
code?: number;
|
|
35
|
-
reason?: string;
|
|
36
|
-
}>;
|
|
37
|
-
/** Assert the next message equals `expected`. */
|
|
38
|
-
assertMessage(expected: string, timeout?: number): Promise<void>;
|
|
39
|
-
/** Assert the connection closes, optionally with `expectedCode`. */
|
|
40
|
-
assertClosed(expectedCode?: number, timeout?: number): Promise<void>;
|
|
41
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// Ported from @stratal/testing (MIT, © Temitayo Fadojutimi). Web-standard
|
|
2
|
-
// `WebSocket` wrapper; the optional `cleanup` hook lets a transport adapter
|
|
3
|
-
// (e.g. websocket-node) tear down its server when the socket closes.
|
|
4
|
-
import { expect } from "vitest";
|
|
5
|
-
/**
|
|
6
|
-
* TestWsConnection
|
|
7
|
-
*
|
|
8
|
-
* Wraps a live `WebSocket` with queue-based wait/assert helpers. Transport is
|
|
9
|
-
* supplied by the caller — the core harness never opens a socket itself (see
|
|
10
|
-
* `@velajs/testing/websocket-node`).
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* const ws = await module.ws('/ws/chat').connect();
|
|
15
|
-
* ws.send('hi');
|
|
16
|
-
* await ws.assertMessage('echo:hi');
|
|
17
|
-
* ws.close();
|
|
18
|
-
* ```
|
|
19
|
-
*/ export class TestWsConnection {
|
|
20
|
-
ws;
|
|
21
|
-
cleanup;
|
|
22
|
-
messageQueue = [];
|
|
23
|
-
messageWaiters = [];
|
|
24
|
-
closeEvent = null;
|
|
25
|
-
closeWaiters = [];
|
|
26
|
-
constructor(ws, cleanup){
|
|
27
|
-
this.ws = ws;
|
|
28
|
-
this.cleanup = cleanup;
|
|
29
|
-
this.ws.addEventListener('message', (event)=>{
|
|
30
|
-
const data = event.data;
|
|
31
|
-
if (this.messageWaiters.length > 0) {
|
|
32
|
-
this.messageWaiters.shift()(data);
|
|
33
|
-
} else {
|
|
34
|
-
this.messageQueue.push(data);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
this.ws.addEventListener('close', (event)=>{
|
|
38
|
-
this.closeEvent = {
|
|
39
|
-
code: event.code,
|
|
40
|
-
reason: event.reason
|
|
41
|
-
};
|
|
42
|
-
for (const waiter of this.closeWaiters){
|
|
43
|
-
waiter(this.closeEvent);
|
|
44
|
-
}
|
|
45
|
-
this.closeWaiters = [];
|
|
46
|
-
void this.cleanup?.();
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
/** The raw `WebSocket`. */ get raw() {
|
|
50
|
-
return this.ws;
|
|
51
|
-
}
|
|
52
|
-
/** Send a message. */ send(data) {
|
|
53
|
-
// The runtime WebSocket.send accepts all of these; the cast bridges the
|
|
54
|
-
// slight type differences between DOM/undici and Bun's WebSocket typings.
|
|
55
|
-
this.ws.send(data);
|
|
56
|
-
}
|
|
57
|
-
/** Close the connection. */ close(code, reason) {
|
|
58
|
-
this.ws.close(code, reason);
|
|
59
|
-
}
|
|
60
|
-
/** Wait for the next message (rejects after `timeout` ms). */ async waitForMessage(timeout = 5000) {
|
|
61
|
-
if (this.messageQueue.length > 0) {
|
|
62
|
-
return this.messageQueue.shift();
|
|
63
|
-
}
|
|
64
|
-
return new Promise((resolve, reject)=>{
|
|
65
|
-
const waiter = (data)=>{
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
resolve(data);
|
|
68
|
-
};
|
|
69
|
-
const timer = setTimeout(()=>{
|
|
70
|
-
const index = this.messageWaiters.indexOf(waiter);
|
|
71
|
-
if (index !== -1) this.messageWaiters.splice(index, 1);
|
|
72
|
-
reject(new Error(`WebSocket: no message received within ${timeout}ms`));
|
|
73
|
-
}, timeout);
|
|
74
|
-
this.messageWaiters.push(waiter);
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
/** Wait for the connection to close (rejects after `timeout` ms). */ async waitForClose(timeout = 5000) {
|
|
78
|
-
if (this.closeEvent) {
|
|
79
|
-
return this.closeEvent;
|
|
80
|
-
}
|
|
81
|
-
return new Promise((resolve, reject)=>{
|
|
82
|
-
const waiter = (event)=>{
|
|
83
|
-
clearTimeout(timer);
|
|
84
|
-
resolve(event);
|
|
85
|
-
};
|
|
86
|
-
const timer = setTimeout(()=>{
|
|
87
|
-
const index = this.closeWaiters.indexOf(waiter);
|
|
88
|
-
if (index !== -1) this.closeWaiters.splice(index, 1);
|
|
89
|
-
reject(new Error(`WebSocket: connection did not close within ${timeout}ms`));
|
|
90
|
-
}, timeout);
|
|
91
|
-
this.closeWaiters.push(waiter);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/** Assert the next message equals `expected`. */ async assertMessage(expected, timeout = 5000) {
|
|
95
|
-
const data = await this.waitForMessage(timeout);
|
|
96
|
-
const message = typeof data === 'string' ? data : '[ArrayBuffer]';
|
|
97
|
-
expect(message, `Expected WebSocket message "${expected}", got "${message}"`).toBe(expected);
|
|
98
|
-
}
|
|
99
|
-
/** Assert the connection closes, optionally with `expectedCode`. */ async assertClosed(expectedCode, timeout = 5000) {
|
|
100
|
-
const event = await this.waitForClose(timeout);
|
|
101
|
-
if (expectedCode !== undefined) {
|
|
102
|
-
expect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ActingAsResolver, TestPrincipal, TestingModule } from '../testing-module.js';
|
|
2
|
-
import type { TestWsConnection } from './test-ws-connection.js';
|
|
3
|
-
/**
|
|
4
|
-
* A transport adapter that performs the WebSocket upgrade and returns a live
|
|
5
|
-
* connection. Registered by a platform package (e.g. websocket-node).
|
|
6
|
-
*/
|
|
7
|
-
export type WsConnector = (module: TestingModule, path: string, headers: Headers) => Promise<TestWsConnection>;
|
|
8
|
-
/**
|
|
9
|
-
* Register the transport that {@link TestWsRequest.connect} uses. Called by a
|
|
10
|
-
* platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).
|
|
11
|
-
*/
|
|
12
|
-
export declare function registerWsConnector(connector: WsConnector): void;
|
|
13
|
-
/** The currently registered WebSocket connector, if any. */
|
|
14
|
-
export declare function getWsConnector(): WsConnector | null;
|
|
15
|
-
/**
|
|
16
|
-
* TestWsRequest
|
|
17
|
-
*
|
|
18
|
-
* Builder for a WebSocket connection. `connect()` requires a transport adapter
|
|
19
|
-
* to be registered; without one it throws a clear, actionable error.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* import '@velajs/testing/websocket-node';
|
|
24
|
-
* const ws = await module.ws('/ws/chat').connect();
|
|
25
|
-
* ws.send('hi');
|
|
26
|
-
* await ws.assertMessage('echo:hi');
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export declare class TestWsRequest {
|
|
30
|
-
private readonly path;
|
|
31
|
-
private readonly module;
|
|
32
|
-
private readonly requestHeaders;
|
|
33
|
-
private principal;
|
|
34
|
-
private resolver;
|
|
35
|
-
constructor(path: string, module: TestingModule);
|
|
36
|
-
/** Merge additional headers onto the upgrade request. */
|
|
37
|
-
withHeaders(headers: Record<string, string>): this;
|
|
38
|
-
/** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */
|
|
39
|
-
actingAs(principal: TestPrincipal, resolver?: ActingAsResolver): this;
|
|
40
|
-
/** Open the socket via the registered transport adapter. */
|
|
41
|
-
connect(): Promise<TestWsConnection>;
|
|
42
|
-
private applyAuthentication;
|
|
43
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Adapted from @stratal/testing (MIT, © Temitayo Fadojutimi). Vela's WebSocket
|
|
2
|
-
// transport is pluggable and NOT reachable through a plain in-memory fetch, so
|
|
3
|
-
// the actual upgrade is delegated to a registered connector — provided by
|
|
4
|
-
// `@velajs/testing/websocket-node` for Node. The core harness never promises a
|
|
5
|
-
// universal `connect()`.
|
|
6
|
-
let registeredConnector = null;
|
|
7
|
-
/**
|
|
8
|
-
* Register the transport that {@link TestWsRequest.connect} uses. Called by a
|
|
9
|
-
* platform adapter's side-effect import (e.g. `@velajs/testing/websocket-node`).
|
|
10
|
-
*/ export function registerWsConnector(connector) {
|
|
11
|
-
registeredConnector = connector;
|
|
12
|
-
}
|
|
13
|
-
/** The currently registered WebSocket connector, if any. */ export function getWsConnector() {
|
|
14
|
-
return registeredConnector;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* TestWsRequest
|
|
18
|
-
*
|
|
19
|
-
* Builder for a WebSocket connection. `connect()` requires a transport adapter
|
|
20
|
-
* to be registered; without one it throws a clear, actionable error.
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* import '@velajs/testing/websocket-node';
|
|
25
|
-
* const ws = await module.ws('/ws/chat').connect();
|
|
26
|
-
* ws.send('hi');
|
|
27
|
-
* await ws.assertMessage('echo:hi');
|
|
28
|
-
* ```
|
|
29
|
-
*/ export class TestWsRequest {
|
|
30
|
-
path;
|
|
31
|
-
module;
|
|
32
|
-
requestHeaders = new Headers();
|
|
33
|
-
principal = null;
|
|
34
|
-
resolver = null;
|
|
35
|
-
constructor(path, module){
|
|
36
|
-
this.path = path;
|
|
37
|
-
this.module = module;
|
|
38
|
-
}
|
|
39
|
-
/** Merge additional headers onto the upgrade request. */ withHeaders(headers) {
|
|
40
|
-
for (const [key, value] of Object.entries(headers)){
|
|
41
|
-
this.requestHeaders.set(key, value);
|
|
42
|
-
}
|
|
43
|
-
return this;
|
|
44
|
-
}
|
|
45
|
-
/** Authenticate the connection (see {@link TestHttpRequest.actingAs}). */ actingAs(principal, resolver) {
|
|
46
|
-
this.principal = principal;
|
|
47
|
-
this.resolver = resolver ?? null;
|
|
48
|
-
return this;
|
|
49
|
-
}
|
|
50
|
-
/** Open the socket via the registered transport adapter. */ async connect() {
|
|
51
|
-
const connector = getWsConnector();
|
|
52
|
-
if (!connector) {
|
|
53
|
-
throw new Error('WebSocket connect() requires a transport adapter. Import ' + '"@velajs/testing/websocket-node" (Node). Cloudflare Durable-Object ' + 'WebSockets are exercised via @velajs/cloudflare + the workerd pool, ' + 'not the core harness.');
|
|
54
|
-
}
|
|
55
|
-
await this.applyAuthentication();
|
|
56
|
-
return connector(this.module, this.path, this.requestHeaders);
|
|
57
|
-
}
|
|
58
|
-
async applyAuthentication() {
|
|
59
|
-
if (!this.principal) return;
|
|
60
|
-
const resolver = this.resolver ?? this.module.getAuthResolver();
|
|
61
|
-
if (!resolver) {
|
|
62
|
-
throw new Error('actingAs() requires an auth resolver. Pass one explicitly or register ' + 'a default with module.setAuthResolver(resolver).');
|
|
63
|
-
}
|
|
64
|
-
const headers = await resolver(this.module, this.principal);
|
|
65
|
-
for (const [key, value] of headers.entries()){
|
|
66
|
-
this.requestHeaders.set(key, value);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|