@strav/testing 0.4.30 → 1.0.0-alpha.24
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/README.md +34 -71
- package/package.json +22 -29
- package/src/boot_test_app.ts +169 -0
- package/src/brain/index.ts +1 -0
- package/src/brain/stub_brain_provider.ts +72 -0
- package/src/compose_test_config.ts +47 -0
- package/src/index.ts +27 -15
- package/src/mem_stream.ts +50 -0
- package/src/postgres/connected_role_bypasses_rls.ts +22 -0
- package/src/postgres/create_test_database.ts +20 -0
- package/src/postgres/index.ts +5 -0
- package/src/postgres/is_postgres_available.ts +33 -0
- package/src/postgres/reset_schema.ts +14 -0
- package/src/postgres/test_database_url.ts +33 -0
- package/src/stub_fetch.ts +56 -0
- package/src/tenant_manager_provider.ts +40 -0
- package/CHANGELOG.md +0 -7
- package/src/browser/db_fresh.ts +0 -38
- package/src/browser/demo_flow.ts +0 -89
- package/src/browser/index.ts +0 -11
- package/src/browser/server_lifecycle.ts +0 -42
- package/src/browser/test_case.ts +0 -572
- package/src/database_manager.ts +0 -131
- package/src/factory.ts +0 -68
- package/src/test_case.ts +0 -312
- package/tsconfig.json +0 -5
package/src/database_manager.ts
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { app } from '@strav/kernel'
|
|
2
|
-
import { Database, BaseModel } from '@strav/database'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Singleton database manager for test isolation
|
|
6
|
-
*
|
|
7
|
-
* Manages a shared database connection across multiple test files
|
|
8
|
-
* while ensuring proper cleanup and preventing "Connection closed" errors.
|
|
9
|
-
*/
|
|
10
|
-
export class TestDatabaseManager {
|
|
11
|
-
private static instance: TestDatabaseManager | null = null
|
|
12
|
-
private database: Database | null = null
|
|
13
|
-
private referenceCount = 0
|
|
14
|
-
private isInitialized = false
|
|
15
|
-
private closeTimeout: Timer | null = null
|
|
16
|
-
private readonly CLOSE_DELAY_MS = 100 // Small delay to allow other test files to start
|
|
17
|
-
|
|
18
|
-
private constructor() {
|
|
19
|
-
// Private constructor for singleton pattern
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Get the singleton instance
|
|
24
|
-
*/
|
|
25
|
-
static getInstance(): TestDatabaseManager {
|
|
26
|
-
if (!TestDatabaseManager.instance) {
|
|
27
|
-
TestDatabaseManager.instance = new TestDatabaseManager()
|
|
28
|
-
}
|
|
29
|
-
return TestDatabaseManager.instance
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get a shared database instance
|
|
34
|
-
* Increments reference count to track active users
|
|
35
|
-
*/
|
|
36
|
-
async getDatabase(): Promise<Database> {
|
|
37
|
-
this.referenceCount++
|
|
38
|
-
|
|
39
|
-
// Cancel any pending close operation
|
|
40
|
-
if (this.closeTimeout) {
|
|
41
|
-
clearTimeout(this.closeTimeout)
|
|
42
|
-
this.closeTimeout = null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (!this.database || !this.isInitialized) {
|
|
46
|
-
await this.initializeDatabase()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return this.database!
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Release the database reference
|
|
54
|
-
* Decrements reference count and closes connection when no more users
|
|
55
|
-
*/
|
|
56
|
-
async releaseDatabase(): Promise<void> {
|
|
57
|
-
this.referenceCount--
|
|
58
|
-
|
|
59
|
-
if (this.referenceCount <= 0 && this.database) {
|
|
60
|
-
// Schedule database close with a small delay
|
|
61
|
-
// This allows other test files to start and increment the reference count
|
|
62
|
-
this.closeTimeout = setTimeout(async () => {
|
|
63
|
-
if (this.referenceCount <= 0 && this.database) {
|
|
64
|
-
await this.database.close()
|
|
65
|
-
this.database = null
|
|
66
|
-
this.isInitialized = false
|
|
67
|
-
this.referenceCount = 0
|
|
68
|
-
}
|
|
69
|
-
this.closeTimeout = null
|
|
70
|
-
}, this.CLOSE_DELAY_MS)
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Initialize the shared database connection
|
|
76
|
-
*/
|
|
77
|
-
private async initializeDatabase(): Promise<void> {
|
|
78
|
-
if (this.isInitialized && this.database) {
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Register database singleton if not already registered
|
|
83
|
-
if (!app.has(Database)) {
|
|
84
|
-
app.singleton(Database)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.database = app.resolve(Database)
|
|
88
|
-
await this.database.init()
|
|
89
|
-
|
|
90
|
-
// Configure BaseModel with the shared database
|
|
91
|
-
new BaseModel(this.database)
|
|
92
|
-
|
|
93
|
-
this.isInitialized = true
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get current reference count (for debugging)
|
|
98
|
-
*/
|
|
99
|
-
getReferenceCount(): number {
|
|
100
|
-
return this.referenceCount
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Force close the database connection (for cleanup)
|
|
105
|
-
*/
|
|
106
|
-
async forceClose(): Promise<void> {
|
|
107
|
-
if (this.database) {
|
|
108
|
-
await this.database.close()
|
|
109
|
-
this.database = null
|
|
110
|
-
this.isInitialized = false
|
|
111
|
-
this.referenceCount = 0
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Utility function to clean up database connections on process exit
|
|
118
|
-
*/
|
|
119
|
-
export function cleanupTestDatabase(): void {
|
|
120
|
-
const manager = TestDatabaseManager.getInstance()
|
|
121
|
-
|
|
122
|
-
process.on('SIGINT', async () => {
|
|
123
|
-
await manager.forceClose()
|
|
124
|
-
process.exit(0)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
process.on('SIGTERM', async () => {
|
|
128
|
-
await manager.forceClose()
|
|
129
|
-
process.exit(0)
|
|
130
|
-
})
|
|
131
|
-
}
|
package/src/factory.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type { BaseModel } from '@strav/database'
|
|
2
|
-
|
|
3
|
-
type ModelClass = typeof BaseModel & { create(attrs: Record<string, unknown>): Promise<any> }
|
|
4
|
-
type DefinitionFn = (seq: number) => Record<string, unknown>
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Lightweight model factory for test seeding.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const UserFactory = Factory.define(User, (seq) => ({
|
|
11
|
-
* pid: crypto.randomUUID(),
|
|
12
|
-
* name: `User ${seq}`,
|
|
13
|
-
* email: `user-${seq}@test.com`,
|
|
14
|
-
* passwordHash: 'hashed',
|
|
15
|
-
* }))
|
|
16
|
-
*
|
|
17
|
-
* const user = await UserFactory.create()
|
|
18
|
-
* const users = await UserFactory.createMany(5)
|
|
19
|
-
* const user = await UserFactory.create({ name: 'Custom' })
|
|
20
|
-
* const instance = UserFactory.make() // in-memory, no DB
|
|
21
|
-
*/
|
|
22
|
-
export class Factory<T extends BaseModel = BaseModel> {
|
|
23
|
-
private static _seq = new Map<Function, number>()
|
|
24
|
-
|
|
25
|
-
private constructor(
|
|
26
|
-
private model: ModelClass,
|
|
27
|
-
private definition: DefinitionFn
|
|
28
|
-
) {}
|
|
29
|
-
|
|
30
|
-
/** Define a factory for a model class. */
|
|
31
|
-
static define<M extends BaseModel>(
|
|
32
|
-
model: typeof BaseModel,
|
|
33
|
-
definition: (seq: number) => Record<string, unknown>
|
|
34
|
-
): Factory<M> {
|
|
35
|
-
return new Factory<M>(model as ModelClass, definition)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private nextSeq(): number {
|
|
39
|
-
const current = Factory._seq.get(this.model) ?? 0
|
|
40
|
-
const next = current + 1
|
|
41
|
-
Factory._seq.set(this.model, next)
|
|
42
|
-
return next
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Create and persist a single record. */
|
|
46
|
-
async create(overrides?: Record<string, unknown>): Promise<T> {
|
|
47
|
-
const attrs = { ...this.definition(this.nextSeq()), ...overrides }
|
|
48
|
-
return this.model.create(attrs) as Promise<T>
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Create and persist multiple records. */
|
|
52
|
-
async createMany(count: number, overrides?: Record<string, unknown>): Promise<T[]> {
|
|
53
|
-
return Promise.all(Array.from({ length: count }, () => this.create(overrides)))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Build an in-memory instance without persisting to the database. */
|
|
57
|
-
make(overrides?: Record<string, unknown>): T {
|
|
58
|
-
const attrs = { ...this.definition(this.nextSeq()), ...overrides }
|
|
59
|
-
const instance = new (this.model as any)()
|
|
60
|
-
instance.merge(attrs)
|
|
61
|
-
return instance as T
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Reset all factory sequences (call between test suites if needed). */
|
|
65
|
-
static resetSequences(): void {
|
|
66
|
-
Factory._seq.clear()
|
|
67
|
-
}
|
|
68
|
-
}
|
package/src/test_case.ts
DELETED
|
@@ -1,312 +0,0 @@
|
|
|
1
|
-
import type { SQL, ReservedSQL } from 'bun'
|
|
2
|
-
import { app, Configuration, ExceptionHandler } from '@strav/kernel'
|
|
3
|
-
import { Database, BaseModel } from '@strav/database'
|
|
4
|
-
import { Router } from '@strav/http'
|
|
5
|
-
import { Factory } from './factory.ts'
|
|
6
|
-
import { TestDatabaseManager } from './database_manager.ts'
|
|
7
|
-
|
|
8
|
-
export interface TestCaseOptions {
|
|
9
|
-
/** Route loader — called during setup to register routes. */
|
|
10
|
-
routes?: () => Promise<unknown>
|
|
11
|
-
/** Boot auth + session tables (default: false). */
|
|
12
|
-
auth?: boolean
|
|
13
|
-
/** Boot view engine (default: false). */
|
|
14
|
-
views?: boolean
|
|
15
|
-
/** Wrap each test in a DB transaction that auto-rollbacks (default: true). */
|
|
16
|
-
transaction?: boolean
|
|
17
|
-
/** User resolver for Auth.useResolver() (required when auth: true). */
|
|
18
|
-
userResolver?: (id: string | number) => Promise<unknown>
|
|
19
|
-
/** Base domain for subdomain extraction (default: 'localhost'). */
|
|
20
|
-
domain?: string
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Boot the app, provide HTTP helpers, and wrap each test in a rolled-back
|
|
25
|
-
* transaction for full isolation.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* import { TestCase, Factory } from '@strav/testing'
|
|
29
|
-
*
|
|
30
|
-
* const t = await TestCase.boot({
|
|
31
|
-
* auth: true,
|
|
32
|
-
* domain: 'example.com',
|
|
33
|
-
* routes: () => import('../start/api_routes'),
|
|
34
|
-
* })
|
|
35
|
-
*
|
|
36
|
-
* describe('Posts', () => {
|
|
37
|
-
* test('list', async () => {
|
|
38
|
-
* const user = await UserFactory.create()
|
|
39
|
-
* await t.actingAs(user)
|
|
40
|
-
* const res = await t.get('/api/posts')
|
|
41
|
-
* expect(res.status).toBe(200)
|
|
42
|
-
* })
|
|
43
|
-
* })
|
|
44
|
-
*/
|
|
45
|
-
export class TestCase {
|
|
46
|
-
db!: Database
|
|
47
|
-
router!: Router
|
|
48
|
-
config!: Configuration
|
|
49
|
-
|
|
50
|
-
private _token: string | null = null
|
|
51
|
-
private _headers: Record<string, string> = {}
|
|
52
|
-
private _originalSql: SQL | null = null
|
|
53
|
-
private _reserved: ReservedSQL | null = null
|
|
54
|
-
private _subdomain: string | null = null
|
|
55
|
-
private _domain: string
|
|
56
|
-
|
|
57
|
-
constructor(private options: TestCaseOptions = {}) {
|
|
58
|
-
this._domain = options.domain || 'localhost'
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Lifecycle
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
/** Boot the app — mirrors index.ts bootstrap, minus Server. Call in beforeAll. */
|
|
66
|
-
async setup(): Promise<void> {
|
|
67
|
-
if (!app.has(Configuration)) app.singleton(Configuration)
|
|
68
|
-
if (!app.has(Router)) app.singleton(Router)
|
|
69
|
-
|
|
70
|
-
this.config = app.resolve(Configuration)
|
|
71
|
-
await this.config.load()
|
|
72
|
-
|
|
73
|
-
// Use shared database manager to prevent connection closed errors
|
|
74
|
-
const dbManager = TestDatabaseManager.getInstance()
|
|
75
|
-
this.db = await dbManager.getDatabase()
|
|
76
|
-
|
|
77
|
-
this.router = app.resolve(Router)
|
|
78
|
-
this.router.setDomain(this._domain)
|
|
79
|
-
|
|
80
|
-
// Auth + Session
|
|
81
|
-
if (this.options.auth) {
|
|
82
|
-
const { SessionManager } = await import('@strav/http')
|
|
83
|
-
const { Auth } = await import('@strav/http')
|
|
84
|
-
const { PostgresSessionStore } = await import('@strav/database')
|
|
85
|
-
|
|
86
|
-
if (!app.has(SessionManager)) app.singleton(SessionManager)
|
|
87
|
-
if (!app.has(Auth)) app.singleton(Auth)
|
|
88
|
-
if (!app.has(PostgresSessionStore)) app.singleton(PostgresSessionStore)
|
|
89
|
-
|
|
90
|
-
app.resolve(SessionManager)
|
|
91
|
-
const sessionStore = app.resolve(PostgresSessionStore)
|
|
92
|
-
SessionManager.useStore(sessionStore)
|
|
93
|
-
await sessionStore.ensureSchema()
|
|
94
|
-
|
|
95
|
-
app.resolve(Auth)
|
|
96
|
-
await Auth.ensureTables()
|
|
97
|
-
|
|
98
|
-
if (this.options.userResolver) {
|
|
99
|
-
Auth.useResolver(this.options.userResolver)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// View engine
|
|
104
|
-
if (this.options.views) {
|
|
105
|
-
const { ViewEngine } = await import('@strav/view')
|
|
106
|
-
const { Context } = await import('@strav/http')
|
|
107
|
-
|
|
108
|
-
if (!app.has(ViewEngine)) app.singleton(ViewEngine)
|
|
109
|
-
const viewEngine = app.resolve(ViewEngine)
|
|
110
|
-
Context.setViewEngine(viewEngine)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Routes
|
|
114
|
-
if (this.options.routes) {
|
|
115
|
-
await this.options.routes()
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Exception handler (always dev mode in tests)
|
|
119
|
-
const handler = new ExceptionHandler(true)
|
|
120
|
-
this.router.useExceptionHandler(handler)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Close the database connection. Call in afterAll. */
|
|
124
|
-
async teardown(): Promise<void> {
|
|
125
|
-
// Ensure any reserved connection is released first
|
|
126
|
-
if (this._reserved) {
|
|
127
|
-
try {
|
|
128
|
-
await this._reserved`ROLLBACK`
|
|
129
|
-
} catch {
|
|
130
|
-
/* ignore */
|
|
131
|
-
}
|
|
132
|
-
this._reserved.release()
|
|
133
|
-
this._reserved = null
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Release database reference instead of closing it directly
|
|
137
|
-
// This allows multiple test files to share the same connection
|
|
138
|
-
const dbManager = TestDatabaseManager.getInstance()
|
|
139
|
-
await dbManager.releaseDatabase()
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Begin a transaction for test isolation. Call in beforeEach. */
|
|
143
|
-
async beforeEach(): Promise<void> {
|
|
144
|
-
if (this.options.transaction !== false) {
|
|
145
|
-
this._originalSql = this.db.sql
|
|
146
|
-
this._reserved = await this._originalSql.reserve()
|
|
147
|
-
await this._reserved`BEGIN`
|
|
148
|
-
|
|
149
|
-
// Monkey-patch Database to use the reserved connection.
|
|
150
|
-
// TypeScript `private` is compile-time only — runtime access works.
|
|
151
|
-
;(this.db as any).appConnection = this._reserved
|
|
152
|
-
;(Database as any)._appConnection = this._reserved
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Rollback the transaction and restore state. Call in afterEach. */
|
|
157
|
-
async afterEach(): Promise<void> {
|
|
158
|
-
if (this._reserved) {
|
|
159
|
-
await this._reserved`ROLLBACK`
|
|
160
|
-
this._reserved.release()
|
|
161
|
-
|
|
162
|
-
// Restore original connection
|
|
163
|
-
;(this.db as any).appConnection = this._originalSql
|
|
164
|
-
;(Database as any)._appConnection = this._originalSql
|
|
165
|
-
this._reserved = null
|
|
166
|
-
this._originalSql = null
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Clear per-test state
|
|
170
|
-
this._token = null
|
|
171
|
-
this._headers = {}
|
|
172
|
-
this._subdomain = null
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// HTTP helpers
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
|
|
179
|
-
/** Send a GET request through the router. */
|
|
180
|
-
get(path: string, headers?: Bun.HeadersInit): Promise<Response> {
|
|
181
|
-
return this.request('GET', path, undefined, headers)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/** Send a POST request with JSON body through the router. */
|
|
185
|
-
post(path: string, body?: unknown, headers?: Bun.HeadersInit): Promise<Response> {
|
|
186
|
-
return this.request('POST', path, body, headers)
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Send a PUT request with JSON body through the router. */
|
|
190
|
-
put(path: string, body?: unknown, headers?: Bun.HeadersInit): Promise<Response> {
|
|
191
|
-
return this.request('PUT', path, body, headers)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Send a PATCH request with JSON body through the router. */
|
|
195
|
-
patch(path: string, body?: unknown, headers?: Bun.HeadersInit): Promise<Response> {
|
|
196
|
-
return this.request('PATCH', path, body, headers)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Send a DELETE request through the router. */
|
|
200
|
-
delete(path: string, headers?: Bun.HeadersInit): Promise<Response> {
|
|
201
|
-
return this.request('DELETE', path, undefined, headers)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
// Auth helpers
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Authenticate as the given user for subsequent requests in this test.
|
|
210
|
-
* Creates a real AccessToken in the database.
|
|
211
|
-
*/
|
|
212
|
-
async actingAs(user: unknown, tokenName = 'test-token'): Promise<this> {
|
|
213
|
-
const { AccessToken } = await import('@strav/http')
|
|
214
|
-
const { token } = await AccessToken.create(user, tokenName)
|
|
215
|
-
this._token = token
|
|
216
|
-
return this
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/** Add custom headers to all subsequent requests in this test. */
|
|
220
|
-
withHeaders(headers: Record<string, string>): this {
|
|
221
|
-
Object.assign(this._headers, headers)
|
|
222
|
-
return this
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** Clear the auth token for the next request. */
|
|
226
|
-
withoutAuth(): this {
|
|
227
|
-
this._token = null
|
|
228
|
-
return this
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Set subdomain for subsequent requests in this test. */
|
|
232
|
-
onSubdomain(subdomain: string): this {
|
|
233
|
-
this._subdomain = subdomain
|
|
234
|
-
return this
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Clear subdomain for subsequent requests. */
|
|
238
|
-
withoutSubdomain(): this {
|
|
239
|
-
this._subdomain = null
|
|
240
|
-
return this
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
// Static shorthand
|
|
245
|
-
// ---------------------------------------------------------------------------
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Boot the TestCase and register bun:test lifecycle hooks automatically.
|
|
249
|
-
*
|
|
250
|
-
* @example
|
|
251
|
-
* const t = await TestCase.boot({
|
|
252
|
-
* auth: true,
|
|
253
|
-
* domain: 'example.com',
|
|
254
|
-
* routes: () => import('../start/api_routes'),
|
|
255
|
-
* })
|
|
256
|
-
*/
|
|
257
|
-
static async boot(options?: TestCaseOptions): Promise<TestCase> {
|
|
258
|
-
const tc = new TestCase(options)
|
|
259
|
-
await tc.setup()
|
|
260
|
-
|
|
261
|
-
const { afterAll, beforeEach, afterEach } = await import('bun:test')
|
|
262
|
-
afterAll(() => tc.teardown())
|
|
263
|
-
beforeEach(() => tc.beforeEach())
|
|
264
|
-
afterEach(() => tc.afterEach())
|
|
265
|
-
|
|
266
|
-
return tc
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// ---------------------------------------------------------------------------
|
|
270
|
-
// Internal
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
|
|
273
|
-
private async request(
|
|
274
|
-
method: string,
|
|
275
|
-
path: string,
|
|
276
|
-
body?: unknown,
|
|
277
|
-
headers?: Bun.HeadersInit
|
|
278
|
-
): Promise<Response> {
|
|
279
|
-
const merged: Record<string, string> = { ...this._headers }
|
|
280
|
-
if (this._token) merged['Authorization'] = `Bearer ${this._token}`
|
|
281
|
-
if (body !== undefined) merged['Content-Type'] = 'application/json'
|
|
282
|
-
|
|
283
|
-
// Set Host header for subdomain routing
|
|
284
|
-
if (this._subdomain) {
|
|
285
|
-
merged['Host'] = `${this._subdomain}.${this._domain}`
|
|
286
|
-
} else {
|
|
287
|
-
merged['Host'] = this._domain
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (headers) {
|
|
291
|
-
const entries =
|
|
292
|
-
headers instanceof Headers
|
|
293
|
-
? Object.fromEntries(headers.entries())
|
|
294
|
-
: Array.isArray(headers)
|
|
295
|
-
? Object.fromEntries(headers)
|
|
296
|
-
: headers
|
|
297
|
-
Object.assign(merged, entries)
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Use the subdomain in the URL when present for clarity
|
|
301
|
-
const hostname = this._subdomain ? `${this._subdomain}.${this._domain}` : this._domain
|
|
302
|
-
const res = this.router.handle(
|
|
303
|
-
new Request(`http://${hostname}${path}`, {
|
|
304
|
-
method,
|
|
305
|
-
headers: merged,
|
|
306
|
-
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
307
|
-
})
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
return (await res) ?? new Response('Not Found', { status: 404 })
|
|
311
|
-
}
|
|
312
|
-
}
|