@strav/testing 0.4.31 → 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/README.md
CHANGED
|
@@ -1,82 +1,45 @@
|
|
|
1
1
|
# @strav/testing
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
bun add -d @strav/testing
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Requires `@strav/core` as a peer dependency.
|
|
12
|
-
|
|
13
|
-
## TestCase
|
|
14
|
-
|
|
15
|
-
Boots the app, provides HTTP helpers, and wraps each test in a rolled-back transaction for full isolation.
|
|
3
|
+
Small, focused testing utilities for Strav apps and the framework itself.
|
|
16
4
|
|
|
17
5
|
```ts
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const user = await UserFactory.create()
|
|
34
|
-
await t.actingAs(user)
|
|
35
|
-
|
|
36
|
-
const res = await t.post('/api/posts', { title: 'Hello' })
|
|
37
|
-
expect(res.status).toBe(201)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
6
|
+
import {
|
|
7
|
+
bootTestApp,
|
|
8
|
+
isPostgresAvailable,
|
|
9
|
+
MemStream,
|
|
10
|
+
stubFetch,
|
|
11
|
+
} from '@strav/testing'
|
|
12
|
+
|
|
13
|
+
if (!await isPostgresAvailable()) {
|
|
14
|
+
test.skip('integration: …', () => {})
|
|
15
|
+
} else {
|
|
16
|
+
// real Postgres flow
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stdout = new MemStream()
|
|
20
|
+
const fetch = stubFetch(async (req) => Response.json({ ok: true }))
|
|
40
21
|
```
|
|
41
22
|
|
|
42
|
-
|
|
23
|
+
Canonical docs live in [`docs/testing/README.md`](../../docs/testing/README.md).
|
|
43
24
|
|
|
44
|
-
|
|
45
|
-
await t.get('/path')
|
|
46
|
-
await t.post('/path', body)
|
|
47
|
-
await t.put('/path', body)
|
|
48
|
-
await t.patch('/path', body)
|
|
49
|
-
await t.delete('/path')
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Auth Helpers
|
|
53
|
-
|
|
54
|
-
```ts
|
|
55
|
-
await t.actingAs(user) // authenticate as user
|
|
56
|
-
t.withHeaders({ 'X-Custom': 'value' })
|
|
57
|
-
t.withoutAuth() // clear auth token
|
|
58
|
-
```
|
|
25
|
+
## What ships
|
|
59
26
|
|
|
60
|
-
|
|
27
|
+
| Surface | Notes |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `bootTestApp({ config, schemas, migrations, providers })` | Replaces the ~50-line `beforeAll` boilerplate every e2e was rolling. Auto-supplies the standard four providers, applies schemas + migrations against `setupDb`, returns `{ app, setupDb, dispose }`. |
|
|
30
|
+
| `composeTestConfig(overrides)` | Merges logger + database defaults with per-test overrides. Used internally by `bootTestApp`; exported for tests that want the config tree without the orchestrator. |
|
|
31
|
+
| `TenantManagerProvider` | Standard 3-line wiring extracted from m5 / m6 / m7. |
|
|
32
|
+
| `MemStream` | In-memory `NodeJS.WritableStream` for asserting on stdout / stderr. Pairs with `ConsoleOutput`. |
|
|
33
|
+
| `stubFetch(handler)` | Typed `fetch` replacement. Confines the `as unknown as typeof fetch` cast to one place. |
|
|
34
|
+
| `isPostgresAvailable()` | Cached probe — returns `false` when env is missing or connection fails. |
|
|
35
|
+
| `createTestDatabase()` | Construct a fresh `PostgresDatabase` from `DB_HOST`/`DB_PORT`/etc. |
|
|
36
|
+
| `resetSchema(db)` | DROP + recreate `public` schema. |
|
|
37
|
+
| `connectedRoleBypassesRls(db)` | True for SUPERUSER / BYPASSRLS roles — tests use it to degrade RLS assertions. |
|
|
38
|
+
| `testDatabaseUrl()` | Returns the Postgres URL or `null` when env is missing. |
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { Factory } from '@strav/testing'
|
|
66
|
-
|
|
67
|
-
const UserFactory = Factory.define(User, (seq) => ({
|
|
68
|
-
pid: crypto.randomUUID(),
|
|
69
|
-
name: `User ${seq}`,
|
|
70
|
-
email: `user-${seq}@test.com`,
|
|
71
|
-
passwordHash: 'hashed',
|
|
72
|
-
}))
|
|
73
|
-
|
|
74
|
-
const user = await UserFactory.create()
|
|
75
|
-
const users = await UserFactory.createMany(5)
|
|
76
|
-
const user = await UserFactory.create({ name: 'Custom' })
|
|
77
|
-
const instance = UserFactory.make() // in-memory only, no DB
|
|
78
|
-
```
|
|
40
|
+
## Subpaths
|
|
79
41
|
|
|
80
|
-
|
|
42
|
+
- `@strav/testing/postgres` — narrow import for just the Postgres helpers.
|
|
43
|
+
- `@strav/testing/brain` — `stubBrainProvider({ embed, model? })`. Requires `@strav/brain` installed (peer-optional).
|
|
81
44
|
|
|
82
|
-
|
|
45
|
+
Deferred to follow-up slices: `stubPaymentDriver`, `stubSocialDriver` — extract when the inline forms in `tests/e2e/m{6,7}-*/` show overlap.
|
package/package.json
CHANGED
|
@@ -1,44 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strav/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-alpha.24",
|
|
4
|
+
"description": "Strav testing utilities — in-memory stream, typed fetch stub, Postgres availability probe + schema reset, bootTestApp orchestrator, stub providers.",
|
|
4
5
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
7
8
|
"exports": {
|
|
8
9
|
".": "./src/index.ts",
|
|
9
|
-
"
|
|
10
|
+
"./brain": "./src/brain/index.ts",
|
|
11
|
+
"./postgres": "./src/postgres/index.ts"
|
|
10
12
|
},
|
|
11
13
|
"files": [
|
|
12
|
-
"src
|
|
13
|
-
"
|
|
14
|
-
"tsconfig.json",
|
|
15
|
-
"CHANGELOG.md"
|
|
14
|
+
"src",
|
|
15
|
+
"README.md"
|
|
16
16
|
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"bun": ">=1.3.14"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@strav/database": "1.0.0-alpha.24",
|
|
25
|
+
"@strav/kernel": "1.0.0-alpha.24"
|
|
26
|
+
},
|
|
17
27
|
"peerDependencies": {
|
|
18
|
-
"@strav/
|
|
19
|
-
"@
|
|
20
|
-
"@strav/view": "0.4.31",
|
|
21
|
-
"@strav/database": "0.4.31",
|
|
22
|
-
"@strav/signal": "0.4.31",
|
|
23
|
-
"@strav/cli": "0.4.31",
|
|
24
|
-
"playwright-core": "^1.45.0"
|
|
28
|
+
"@strav/brain": "1.0.0-alpha.24",
|
|
29
|
+
"@types/bun": ">=1.3.14"
|
|
25
30
|
},
|
|
26
31
|
"peerDependenciesMeta": {
|
|
27
|
-
"@strav/
|
|
28
|
-
"optional": true
|
|
29
|
-
},
|
|
30
|
-
"@strav/cli": {
|
|
31
|
-
"optional": true
|
|
32
|
-
},
|
|
33
|
-
"playwright-core": {
|
|
32
|
+
"@strav/brain": {
|
|
34
33
|
"optional": true
|
|
35
34
|
}
|
|
36
35
|
},
|
|
37
|
-
"
|
|
38
|
-
"test": "bun test tests/",
|
|
39
|
-
"typecheck": "tsc --noEmit"
|
|
40
|
-
},
|
|
41
|
-
"devDependencies": {
|
|
42
|
-
"playwright-core": "^1.45.0"
|
|
43
|
-
}
|
|
36
|
+
"devDependencies": null
|
|
44
37
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot a tested Application with the standard four providers
|
|
3
|
+
* (`ConfigProvider` → `LoggerProvider` → `DatabaseProvider` →
|
|
4
|
+
* `TenantManagerProvider`) pre-installed, real Postgres reset, schemas
|
|
5
|
+
* applied, optional migrations run, and the per-test providers wired
|
|
6
|
+
* on top. Returns the started `app`, the admin-level `setupDb` (kept
|
|
7
|
+
* open until `dispose()`), and the `dispose()` cleanup that pairs with
|
|
8
|
+
* `afterAll`.
|
|
9
|
+
*
|
|
10
|
+
* Replaces the ~50 LOC `beforeAll` boilerplate that every integration
|
|
11
|
+
* / e2e suite was rolling by hand. Per-test variation (custom config
|
|
12
|
+
* sub-trees, schemas, migrations, post-boot `useDriver` hand-wiring,
|
|
13
|
+
* HTTP server spin-up) lives where it always did.
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { afterAll, beforeAll, describe } from 'bun:test'
|
|
17
|
+
* import {
|
|
18
|
+
* bootTestApp,
|
|
19
|
+
* isPostgresAvailable,
|
|
20
|
+
* type BootTestAppResult,
|
|
21
|
+
* } from '@strav/testing'
|
|
22
|
+
* import { RagProvider, ragVectorSchema, applyRagVectorMigration } from '@strav/rag'
|
|
23
|
+
*
|
|
24
|
+
* const PG = await isPostgresAvailable()
|
|
25
|
+
*
|
|
26
|
+
* describe.skipIf(!PG)('M5 e2e', () => {
|
|
27
|
+
* let booted: BootTestAppResult
|
|
28
|
+
* beforeAll(async () => {
|
|
29
|
+
* booted = await bootTestApp({
|
|
30
|
+
* config: { rag: { ... } },
|
|
31
|
+
* schemas: [tenantSchema, articleSchema, ragVectorSchema],
|
|
32
|
+
* migrations: [(db, registry) => applyRagVectorMigration(db, { dimension: 4, registry })],
|
|
33
|
+
* providers: [new StubBrainProvider(), new RagProvider()],
|
|
34
|
+
* })
|
|
35
|
+
* })
|
|
36
|
+
* afterAll(() => booted.dispose())
|
|
37
|
+
*
|
|
38
|
+
* // test cases pull from `booted.app.resolve(...)` and `booted.setupDb`.
|
|
39
|
+
* })
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* The orchestration order, per spec from the e2e survey:
|
|
43
|
+
*
|
|
44
|
+
* 1. `createTestDatabase()` → admin connection for setup.
|
|
45
|
+
* 2. `resetSchema(setupDb)` → DROP + CREATE `public`.
|
|
46
|
+
* 3. Apply each schema's `emitCreateTable(schema, { registry }).sql`.
|
|
47
|
+
* 4. Run each migration with `(setupDb, registry)`.
|
|
48
|
+
* 5. Construct config via `composeTestConfig(config)` (auto-supplies
|
|
49
|
+
* logger + database.url unless overridden).
|
|
50
|
+
* 6. `new Application()`, `useProviders([Config, Logger, Database,
|
|
51
|
+
* TenantManager, ...userProviders])` unless `skipDefaultProviders`.
|
|
52
|
+
* 7. Bind `SchemaRegistry` as a singleton with `schemas` registered.
|
|
53
|
+
* 8. `await app.start({ signalHandlers: false })`.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
DatabaseProvider,
|
|
58
|
+
PostgresDatabase,
|
|
59
|
+
type Schema,
|
|
60
|
+
SchemaRegistry,
|
|
61
|
+
emitCreateTable,
|
|
62
|
+
} from '@strav/database'
|
|
63
|
+
import {
|
|
64
|
+
Application,
|
|
65
|
+
ConfigProvider,
|
|
66
|
+
LoggerProvider,
|
|
67
|
+
type ServiceProvider,
|
|
68
|
+
} from '@strav/kernel'
|
|
69
|
+
import { type ConfigOverrides, composeTestConfig } from './compose_test_config.ts'
|
|
70
|
+
import { createTestDatabase } from './postgres/create_test_database.ts'
|
|
71
|
+
import { resetSchema } from './postgres/reset_schema.ts'
|
|
72
|
+
import { TenantManagerProvider } from './tenant_manager_provider.ts'
|
|
73
|
+
|
|
74
|
+
/** Migration hook signature — runs against `setupDb` before `app.start`. */
|
|
75
|
+
export type TestMigration = (
|
|
76
|
+
db: PostgresDatabase,
|
|
77
|
+
registry: SchemaRegistry,
|
|
78
|
+
) => Promise<void> | void
|
|
79
|
+
|
|
80
|
+
export interface BootTestAppOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Per-test config sub-trees. `logger` and `database` keys are
|
|
83
|
+
* auto-supplied unless overridden here. Other keys (`rag`, `payment`,
|
|
84
|
+
* `social`, `encryption`, …) are passed through verbatim.
|
|
85
|
+
*/
|
|
86
|
+
config?: ConfigOverrides
|
|
87
|
+
/**
|
|
88
|
+
* Schemas registered in the `SchemaRegistry` singleton AND applied
|
|
89
|
+
* via `emitCreateTable(schema, { registry }).sql` against `setupDb`
|
|
90
|
+
* before `app.start`. Order matters when there are FK dependencies.
|
|
91
|
+
*/
|
|
92
|
+
schemas?: readonly Schema[]
|
|
93
|
+
/**
|
|
94
|
+
* Migrations to run after `schemas` are applied. Each receives the
|
|
95
|
+
* admin connection + the registry so it can compose SQL the same way
|
|
96
|
+
* the production migration helpers do.
|
|
97
|
+
*/
|
|
98
|
+
migrations?: readonly TestMigration[]
|
|
99
|
+
/**
|
|
100
|
+
* Per-test service providers — appended after the standard four
|
|
101
|
+
* (`Config`, `Logger`, `Database`, `TenantManager`). Order within
|
|
102
|
+
* this list matters for the kernel's topological sort.
|
|
103
|
+
*/
|
|
104
|
+
providers?: readonly ServiceProvider[]
|
|
105
|
+
/**
|
|
106
|
+
* Opt out of the standard four. Useful for tests that need a custom
|
|
107
|
+
* `LoggerProvider`, no `DatabaseProvider`, etc. When `true`, you own
|
|
108
|
+
* the entire provider list via `providers`. Default `false`.
|
|
109
|
+
*/
|
|
110
|
+
skipDefaultProviders?: boolean
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface BootTestAppResult {
|
|
114
|
+
/** Started Application — call `resolve(X)` from test cases. */
|
|
115
|
+
app: Application
|
|
116
|
+
/**
|
|
117
|
+
* Admin-level Postgres connection used to apply DDL + migrations and
|
|
118
|
+
* to run setup queries (seeding tenants, asserting schema state). Kept
|
|
119
|
+
* open until `dispose()`.
|
|
120
|
+
*/
|
|
121
|
+
setupDb: PostgresDatabase
|
|
122
|
+
/** Cleanup: shutdown app + close setupDb. Use in `afterAll`. */
|
|
123
|
+
dispose(): Promise<void>
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function bootTestApp(options: BootTestAppOptions = {}): Promise<BootTestAppResult> {
|
|
127
|
+
const setupDb = createTestDatabase()
|
|
128
|
+
await resetSchema(setupDb)
|
|
129
|
+
|
|
130
|
+
const schemas = options.schemas ?? []
|
|
131
|
+
const registry = new SchemaRegistry().registerAll(schemas)
|
|
132
|
+
|
|
133
|
+
for (const schema of schemas) {
|
|
134
|
+
await setupDb.execute(emitCreateTable(schema, { registry }).sql)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const migration of options.migrations ?? []) {
|
|
138
|
+
await migration(setupDb, registry)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const configData = composeTestConfig(options.config ?? {})
|
|
142
|
+
|
|
143
|
+
const app = new Application()
|
|
144
|
+
const defaults = options.skipDefaultProviders
|
|
145
|
+
? []
|
|
146
|
+
: [
|
|
147
|
+
new ConfigProvider(configData),
|
|
148
|
+
new LoggerProvider(),
|
|
149
|
+
new DatabaseProvider(),
|
|
150
|
+
new TenantManagerProvider(),
|
|
151
|
+
]
|
|
152
|
+
app.useProviders([...defaults, ...(options.providers ?? [])])
|
|
153
|
+
|
|
154
|
+
// Bind the SchemaRegistry singleton so repositories that need it can
|
|
155
|
+
// resolve through the container instead of getting hand-wired.
|
|
156
|
+
app.singleton(SchemaRegistry, () => registry)
|
|
157
|
+
|
|
158
|
+
await app.start({ signalHandlers: false })
|
|
159
|
+
|
|
160
|
+
let disposed = false
|
|
161
|
+
const dispose = async (): Promise<void> => {
|
|
162
|
+
if (disposed) return
|
|
163
|
+
disposed = true
|
|
164
|
+
await app.shutdown()
|
|
165
|
+
await setupDb.close({ timeout: 2 })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { app, setupDb, dispose }
|
|
169
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { stubBrainProvider, type StubBrainOptions } from './stub_brain_provider.ts'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stub `BrainManager` wired as a `ServiceProvider` for tests that need
|
|
3
|
+
* deterministic embeddings without dialing an actual brain backend.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `tests/e2e/m5-rag/`. The provider declares
|
|
6
|
+
* `name: 'brain'` and `dependencies: ['config']`, matching the real
|
|
7
|
+
* `BrainProvider`'s shape — `RagProvider` (which declares
|
|
8
|
+
* `dependencies: ['config', 'brain']`) resolves the stub correctly
|
|
9
|
+
* when both are in the provider list.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { stubBrainProvider } from '@strav/testing/brain'
|
|
13
|
+
*
|
|
14
|
+
* const provider = stubBrainProvider({
|
|
15
|
+
* embed: (text) => bagOfWords(text), // returns number[]
|
|
16
|
+
* })
|
|
17
|
+
*
|
|
18
|
+
* app.useProviders([
|
|
19
|
+
* new ConfigProvider({ ... }),
|
|
20
|
+
* new LoggerProvider(),
|
|
21
|
+
* new DatabaseProvider(),
|
|
22
|
+
* provider, // ← stub registered here
|
|
23
|
+
* new RagProvider(), // ← resolves the stub for its embed calls
|
|
24
|
+
* ])
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The `embed` callback is per-text — the helper maps over the input
|
|
28
|
+
* array internally. Only `embed` is stubbed; other `BrainManager`
|
|
29
|
+
* surface (`chat`, `stream`, `runWithTools`, …) throws when called.
|
|
30
|
+
* V1 scope is rag-style tests; broader stubs land when a second
|
|
31
|
+
* use case appears.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { BrainManager } from '@strav/brain'
|
|
35
|
+
import { type Application, ServiceProvider } from '@strav/kernel'
|
|
36
|
+
|
|
37
|
+
export interface StubBrainOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Returns the embedding vector for a single text. The provider maps
|
|
40
|
+
* the user's `embed` over `texts` internally to produce `number[][]`.
|
|
41
|
+
*/
|
|
42
|
+
embed: (text: string) => number[]
|
|
43
|
+
/** Model identifier surfaced on `embed` results. Default `'stub'`. */
|
|
44
|
+
model?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function stubBrainProvider(options: StubBrainOptions): ServiceProvider {
|
|
48
|
+
const model = options.model ?? 'stub'
|
|
49
|
+
const userEmbed = options.embed
|
|
50
|
+
return new (class StubBrainProvider extends ServiceProvider {
|
|
51
|
+
override readonly name = 'brain'
|
|
52
|
+
override readonly dependencies = ['config']
|
|
53
|
+
override register(app: Application): void {
|
|
54
|
+
app.singleton(BrainManager, () => buildStub(userEmbed, model))
|
|
55
|
+
}
|
|
56
|
+
})()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildStub(
|
|
60
|
+
embed: (text: string) => number[],
|
|
61
|
+
model: string,
|
|
62
|
+
): BrainManager {
|
|
63
|
+
const stub = {
|
|
64
|
+
embed: async (texts: readonly string[]) => ({
|
|
65
|
+
embeddings: texts.map(embed),
|
|
66
|
+
model,
|
|
67
|
+
usage: { inputTokens: 0 },
|
|
68
|
+
raw: null,
|
|
69
|
+
}),
|
|
70
|
+
}
|
|
71
|
+
return stub as unknown as BrainManager
|
|
72
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge per-test config overrides with the standard test defaults.
|
|
3
|
+
*
|
|
4
|
+
* Defaults supplied:
|
|
5
|
+
*
|
|
6
|
+
* - `logger`: silent + stderr channel — keeps `bun test` output clean.
|
|
7
|
+
* - `database.url`: composed from `DB_*` env vars via `testDatabaseUrl()`.
|
|
8
|
+
*
|
|
9
|
+
* Both are deep-merged with `overrides`. Pass `logger` or `database` in
|
|
10
|
+
* `overrides` to replace the default for that key. Other keys
|
|
11
|
+
* (`rag`, `payment`, `social`, `encryption`, etc.) are taken verbatim
|
|
12
|
+
* from `overrides`.
|
|
13
|
+
*
|
|
14
|
+
* Throws when `DB_*` env vars are missing AND no `database.url` override
|
|
15
|
+
* is supplied — `bootTestApp` callers gate via `isPostgresAvailable()`
|
|
16
|
+
* first, so the throw should only fire in misconfigured environments.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { testDatabaseUrl } from './postgres/test_database_url.ts'
|
|
20
|
+
|
|
21
|
+
export type ConfigOverrides = Record<string, unknown>
|
|
22
|
+
|
|
23
|
+
const DEFAULT_LOGGER = {
|
|
24
|
+
default: 'main',
|
|
25
|
+
level: 'silent',
|
|
26
|
+
channels: { main: { driver: 'stderr' } },
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
export function composeTestConfig(overrides: ConfigOverrides = {}): Record<string, unknown> {
|
|
30
|
+
const merged: Record<string, unknown> = { ...overrides }
|
|
31
|
+
|
|
32
|
+
if (!('logger' in merged)) {
|
|
33
|
+
merged.logger = { ...DEFAULT_LOGGER }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!('database' in merged)) {
|
|
37
|
+
const url = testDatabaseUrl()
|
|
38
|
+
if (url === null) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'composeTestConfig: missing DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE env. Source .env.test, run docker-compose up, or pass `database` explicitly in overrides.',
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
merged.database = { url }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return merged
|
|
47
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// Public API of @strav/testing.
|
|
2
|
+
//
|
|
3
|
+
// V1 ships the small utilities that get re-implemented inline in every
|
|
4
|
+
// test: an in-memory writable stream, a typed fetch stub, and the
|
|
5
|
+
// Postgres availability + reset helpers used by integration suites.
|
|
6
|
+
// V2 (this slice) adds bootTestApp + composeTestConfig +
|
|
7
|
+
// TenantManagerProvider for the e2e boot-dance.
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
export {
|
|
10
|
+
bootTestApp,
|
|
11
|
+
type BootTestAppOptions,
|
|
12
|
+
type BootTestAppResult,
|
|
13
|
+
type TestMigration,
|
|
14
|
+
} from './boot_test_app.ts'
|
|
15
|
+
export { composeTestConfig, type ConfigOverrides } from './compose_test_config.ts'
|
|
16
|
+
export { MemStream } from './mem_stream.ts'
|
|
17
|
+
export { stubFetch, type FetchHandler } from './stub_fetch.ts'
|
|
18
|
+
export { TenantManagerProvider } from './tenant_manager_provider.ts'
|
|
7
19
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from './
|
|
20
|
+
// Postgres helpers — also re-exported under `@strav/testing/postgres`
|
|
21
|
+
// for apps that want to import them without pulling in the rest of
|
|
22
|
+
// the barrel.
|
|
23
|
+
export {
|
|
24
|
+
connectedRoleBypassesRls,
|
|
25
|
+
createTestDatabase,
|
|
26
|
+
isPostgresAvailable,
|
|
27
|
+
resetSchema,
|
|
28
|
+
testDatabaseUrl,
|
|
29
|
+
} from './postgres/index.ts'
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory `WritableStream` for tests that assert on stdout/stderr.
|
|
3
|
+
*
|
|
4
|
+
* Pairs with `@strav/kernel`'s `ConsoleOutput` and `@strav/cli`'s
|
|
5
|
+
* command/`runCli` flows — both accept a `NodeJS.WritableStream`
|
|
6
|
+
* pair, and `MemStream` is the smallest possible double that lets
|
|
7
|
+
* tests inspect what was written.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { MemStream } from '@strav/testing'
|
|
11
|
+
* import { ConsoleOutput } from '@strav/kernel'
|
|
12
|
+
*
|
|
13
|
+
* const stdout = new MemStream()
|
|
14
|
+
* const out = new ConsoleOutput({ stdout: stdout.asWritable(), useColor: false })
|
|
15
|
+
* out.line('hello')
|
|
16
|
+
* expect(stdout.text()).toBe('hello\n')
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* `asWritable()` exists so callers don't need the `as unknown as
|
|
20
|
+
* NodeJS.WritableStream` cast at every test site — the cast is
|
|
21
|
+
* confined to this module's boundary.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export class MemStream {
|
|
25
|
+
readonly chunks: string[] = []
|
|
26
|
+
|
|
27
|
+
write(chunk: string): boolean {
|
|
28
|
+
this.chunks.push(chunk)
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Concatenated written content. */
|
|
33
|
+
text(): string {
|
|
34
|
+
return this.chunks.join('')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Drop everything written so far. Useful between assertions in a single test. */
|
|
38
|
+
clear(): void {
|
|
39
|
+
this.chunks.length = 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Cast to `NodeJS.WritableStream` for APIs that demand the full
|
|
44
|
+
* Node interface (`ConsoleOutput`, child-process stdio, etc.).
|
|
45
|
+
* Confines the boundary cast to this method.
|
|
46
|
+
*/
|
|
47
|
+
asWritable(): NodeJS.WritableStream {
|
|
48
|
+
return this as unknown as NodeJS.WritableStream
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `true` when the connected role is a Postgres SUPERUSER or has the
|
|
3
|
+
* BYPASSRLS attribute. Such roles ignore `ENABLE ROW LEVEL SECURITY` —
|
|
4
|
+
* even with `FORCE ROW LEVEL SECURITY` set on a table — so
|
|
5
|
+
* RLS-isolation assertions can't pass under them. Tests use this to
|
|
6
|
+
* self-skip the RLS-scoping check while still exercising the rest of
|
|
7
|
+
* the tenancy path.
|
|
8
|
+
*
|
|
9
|
+
* Production setups should run the app under a non-privileged role and
|
|
10
|
+
* keep BYPASSRLS reserved for the admin pool (`TenantManager`'s
|
|
11
|
+
* `adminDb`). Local-dev / CI databases often share one superuser for
|
|
12
|
+
* convenience — this helper lets the suite degrade gracefully there.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { PostgresDatabase } from '@strav/database'
|
|
16
|
+
|
|
17
|
+
export async function connectedRoleBypassesRls(db: PostgresDatabase): Promise<boolean> {
|
|
18
|
+
const row = await db.queryOne<{ rolsuper: boolean; rolbypassrls: boolean }>(
|
|
19
|
+
`SELECT rolsuper, rolbypassrls FROM pg_roles WHERE rolname = current_user`,
|
|
20
|
+
)
|
|
21
|
+
return Boolean(row?.rolsuper) || Boolean(row?.rolbypassrls)
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Construct a fresh `PostgresDatabase` against the test connection. The
|
|
3
|
+
* caller owns the close — typically in an `afterAll` hook.
|
|
4
|
+
*
|
|
5
|
+
* Throws when env is missing; tests that don't want the throw should
|
|
6
|
+
* gate via `isPostgresAvailable()` first.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PostgresDatabase } from '@strav/database'
|
|
10
|
+
import { testDatabaseUrl } from './test_database_url.ts'
|
|
11
|
+
|
|
12
|
+
export function createTestDatabase(): PostgresDatabase {
|
|
13
|
+
const url = testDatabaseUrl()
|
|
14
|
+
if (url === null) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'createTestDatabase: missing DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_DATABASE env. Source .env.test or run docker-compose up.',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
return new PostgresDatabase({ url, max: 2 })
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { connectedRoleBypassesRls } from './connected_role_bypasses_rls.ts'
|
|
2
|
+
export { createTestDatabase } from './create_test_database.ts'
|
|
3
|
+
export { isPostgresAvailable } from './is_postgres_available.ts'
|
|
4
|
+
export { resetSchema } from './reset_schema.ts'
|
|
5
|
+
export { testDatabaseUrl } from './test_database_url.ts'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap connection probe with a short timeout. Cached for the lifetime
|
|
3
|
+
* of the test process — connection state doesn't flip mid-suite.
|
|
4
|
+
*
|
|
5
|
+
* Returns `false` if env is missing OR the connection attempt fails
|
|
6
|
+
* (Postgres down, wrong creds, network unreachable). Pair with
|
|
7
|
+
* `describe.skipIf(!await isPostgresAvailable())` so suites that need
|
|
8
|
+
* a real Postgres self-skip when it's not reachable.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { PostgresDatabase } from '@strav/database'
|
|
12
|
+
import { testDatabaseUrl } from './test_database_url.ts'
|
|
13
|
+
|
|
14
|
+
let cachedAvailability: boolean | null = null
|
|
15
|
+
|
|
16
|
+
export async function isPostgresAvailable(): Promise<boolean> {
|
|
17
|
+
if (cachedAvailability !== null) return cachedAvailability
|
|
18
|
+
const url = testDatabaseUrl()
|
|
19
|
+
if (url === null) {
|
|
20
|
+
cachedAvailability = false
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const probe = new PostgresDatabase({ url, max: 1 })
|
|
25
|
+
await probe.queryOne('SELECT 1 AS ok')
|
|
26
|
+
await probe.close({ timeout: 1 })
|
|
27
|
+
cachedAvailability = true
|
|
28
|
+
return true
|
|
29
|
+
} catch {
|
|
30
|
+
cachedAvailability = false
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drop + recreate the `public` schema on the connected database.
|
|
3
|
+
* Bulletproof isolation between integration test runs at the cost of
|
|
4
|
+
* a sledgehammer — the integration test database owns its state and
|
|
5
|
+
* shouldn't be pointed at anything precious.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostgresDatabase } from '@strav/database'
|
|
9
|
+
|
|
10
|
+
export async function resetSchema(db: PostgresDatabase): Promise<void> {
|
|
11
|
+
await db.execute('DROP SCHEMA IF EXISTS public CASCADE')
|
|
12
|
+
await db.execute('CREATE SCHEMA public')
|
|
13
|
+
await db.execute('GRANT ALL ON SCHEMA public TO public')
|
|
14
|
+
}
|