@strav/testing 0.4.31 → 1.0.0-alpha.25
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 +23 -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/cache/index.ts +2 -0
- package/src/cache/is_memcached_available.ts +75 -0
- package/src/cache/is_redis_available.ts +39 -0
- package/src/compose_test_config.ts +47 -0
- package/src/index.ts +31 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the standard env-var contract (`DB_HOST` / `DB_PORT` / `DB_USER` /
|
|
3
|
+
* `DB_PASSWORD` / `DB_DATABASE`) shared with CI and `.env.test` and
|
|
4
|
+
* assembles a Postgres URL. Returns `null` when any required variable
|
|
5
|
+
* is missing — callers self-skip rather than throw, so `bun test` is a
|
|
6
|
+
* no-op for integration tests in environments without local Postgres.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface PgEnv {
|
|
10
|
+
host: string
|
|
11
|
+
port: string
|
|
12
|
+
user: string
|
|
13
|
+
password: string
|
|
14
|
+
database: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readPgEnv(): PgEnv | null {
|
|
18
|
+
const host = process.env.DB_HOST
|
|
19
|
+
const port = process.env.DB_PORT
|
|
20
|
+
const user = process.env.DB_USER
|
|
21
|
+
const password = process.env.DB_PASSWORD
|
|
22
|
+
const database = process.env.DB_DATABASE
|
|
23
|
+
if (!host || !port || !user || !password || !database) return null
|
|
24
|
+
return { host, port, user, password, database }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function testDatabaseUrl(): string | null {
|
|
28
|
+
const env = readPgEnv()
|
|
29
|
+
if (env === null) return null
|
|
30
|
+
// `encodeURIComponent` so a password containing `@` or `:` doesn't
|
|
31
|
+
// corrupt the URL.
|
|
32
|
+
return `postgres://${env.user}:${encodeURIComponent(env.password)}@${env.host}:${env.port}/${env.database}`
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed `fetch` stub for driver tests that need to assert on outgoing
|
|
3
|
+
* HTTP without a network round-trip.
|
|
4
|
+
*
|
|
5
|
+
* Adapters that take a `fetch` injection point (e.g. `LineSocialDriver`,
|
|
6
|
+
* pure-fetch brain drivers) typically end up with `as unknown as typeof
|
|
7
|
+
* fetch` boilerplate in tests because the standard `fetch` signature
|
|
8
|
+
* (with `preconnect`, etc.) doesn't match a plain async function.
|
|
9
|
+
* `stubFetch` confines that cast to one place.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { stubFetch } from '@strav/testing'
|
|
13
|
+
*
|
|
14
|
+
* const captured: Request[] = []
|
|
15
|
+
* const driver = new LineSocialDriver({
|
|
16
|
+
* config: { ... },
|
|
17
|
+
* fetch: stubFetch(async (req) => {
|
|
18
|
+
* captured.push(req)
|
|
19
|
+
* if (req.url.includes('/token')) {
|
|
20
|
+
* return Response.json({ access_token: 'AT_1', expires_in: 3600 })
|
|
21
|
+
* }
|
|
22
|
+
* return new Response('not found', { status: 404 })
|
|
23
|
+
* }),
|
|
24
|
+
* })
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* The handler receives a `Request` regardless of how the caller invoked
|
|
28
|
+
* `fetch` (URL + init, URL string, existing Request). Normalization
|
|
29
|
+
* happens here so the handler doesn't have to branch on input shape.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type FetchHandler = (request: Request) => Response | Promise<Response>
|
|
33
|
+
|
|
34
|
+
export function stubFetch(handler: FetchHandler): typeof fetch {
|
|
35
|
+
const fn = async (
|
|
36
|
+
input: Parameters<typeof fetch>[0],
|
|
37
|
+
init?: Parameters<typeof fetch>[1],
|
|
38
|
+
): Promise<Response> => {
|
|
39
|
+
const request = normalizeToRequest(input, init)
|
|
40
|
+
return handler(request)
|
|
41
|
+
}
|
|
42
|
+
return fn as unknown as typeof fetch
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeToRequest(
|
|
46
|
+
input: Parameters<typeof fetch>[0],
|
|
47
|
+
init: Parameters<typeof fetch>[1],
|
|
48
|
+
): Request {
|
|
49
|
+
if (input instanceof Request) {
|
|
50
|
+
// If `init` is provided, the spec says we layer it on; new Request
|
|
51
|
+
// accepts (Request, RequestInit) and merges accordingly.
|
|
52
|
+
return init === undefined ? input : new Request(input, init)
|
|
53
|
+
}
|
|
54
|
+
// string | URL → construct a Request. URL converts via its toString.
|
|
55
|
+
return new Request(typeof input === 'string' ? input : input.toString(), init)
|
|
56
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard `TenantManager` wiring for tests.
|
|
3
|
+
*
|
|
4
|
+
* `DatabaseProvider` doesn't auto-register `TenantManager` — apps wire
|
|
5
|
+
* it themselves so the binding is explicit (and so apps that don't use
|
|
6
|
+
* tenancy don't pay for it). Every integration / e2e suite that uses
|
|
7
|
+
* `TenantManager` defined an identical 3-line `ServiceProvider` to do
|
|
8
|
+
* the wiring; this is the extracted version.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { TenantManagerProvider } from '@strav/testing'
|
|
12
|
+
*
|
|
13
|
+
* app.useProviders([
|
|
14
|
+
* new ConfigProvider({ ... }),
|
|
15
|
+
* new LoggerProvider(),
|
|
16
|
+
* new DatabaseProvider(),
|
|
17
|
+
* new TenantManagerProvider(),
|
|
18
|
+
* // your provider here
|
|
19
|
+
* ])
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The class implements the same shape every e2e was rolling by hand:
|
|
23
|
+
* declares `database` as a dependency, binds `TenantManager` as a
|
|
24
|
+
* singleton built from the resolved `PostgresDatabase` + `EventBus`.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { PostgresDatabase, TenantManager } from '@strav/database'
|
|
28
|
+
import { type Application, EventBus, ServiceProvider } from '@strav/kernel'
|
|
29
|
+
|
|
30
|
+
export class TenantManagerProvider extends ServiceProvider {
|
|
31
|
+
override readonly name = 'tenant'
|
|
32
|
+
override readonly dependencies = ['database']
|
|
33
|
+
|
|
34
|
+
override register(app: Application): void {
|
|
35
|
+
app.singleton(
|
|
36
|
+
TenantManager,
|
|
37
|
+
(c) => new TenantManager(c.resolve(PostgresDatabase), c.resolve(EventBus)),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
package/CHANGELOG.md
DELETED
package/src/browser/db_fresh.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Programmatic equivalent of `bun strav fresh`. Refuses to run unless the
|
|
3
|
-
* caller has set APP_ENV to 'test' or 'local' — the CLI command requires
|
|
4
|
-
* 'local'; we additionally accept 'test' so test runners don't need a flag
|
|
5
|
-
* dance. Lazy-imports `@strav/cli` so packages that don't use BrowserTestCase
|
|
6
|
-
* don't pay the dependency cost.
|
|
7
|
-
*/
|
|
8
|
-
export async function runFresh(): Promise<void> {
|
|
9
|
-
const env = process.env.APP_ENV
|
|
10
|
-
if (env !== 'test' && env !== 'local') {
|
|
11
|
-
throw new Error(
|
|
12
|
-
`runFresh refused: APP_ENV must be 'test' or 'local' (got '${env ?? '<unset>'}'). ` +
|
|
13
|
-
`This is a safety guard — fresh wipes all tables.`
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
let freshDatabase: typeof import('@strav/cli/commands/migration_fresh').freshDatabase
|
|
18
|
-
let bootstrap: typeof import('@strav/cli/cli/bootstrap').bootstrap
|
|
19
|
-
let shutdown: typeof import('@strav/cli/cli/bootstrap').shutdown
|
|
20
|
-
let getDatabasePaths: typeof import('@strav/cli/config/loader').getDatabasePaths
|
|
21
|
-
try {
|
|
22
|
-
;({ freshDatabase } = await import('@strav/cli/commands/migration_fresh'))
|
|
23
|
-
;({ bootstrap, shutdown } = await import('@strav/cli/cli/bootstrap'))
|
|
24
|
-
;({ getDatabasePaths } = await import('@strav/cli/config/loader'))
|
|
25
|
-
} catch (err) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
`runFresh requires @strav/cli to be installed in this workspace. Add it as a peer dependency. (${(err as Error).message})`
|
|
28
|
-
)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const paths = await getDatabasePaths()
|
|
32
|
-
const { db, registry, introspector } = await bootstrap()
|
|
33
|
-
try {
|
|
34
|
-
await freshDatabase(db, registry, introspector, paths.migrations)
|
|
35
|
-
} finally {
|
|
36
|
-
await shutdown(db)
|
|
37
|
-
}
|
|
38
|
-
}
|
package/src/browser/demo_flow.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import type { Page } from 'playwright-core'
|
|
2
|
-
import { BrowserTestCase } from './test_case.ts'
|
|
3
|
-
import type { BrowserTestCaseOptions } from './test_case.ts'
|
|
4
|
-
|
|
5
|
-
export interface DemoFlowOptions extends BrowserTestCaseOptions {
|
|
6
|
-
/** Default: '/auth/magic' — the conventional magic-link endpoint. */
|
|
7
|
-
signInEndpoint?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Opinionated wrapper around {@link BrowserTestCase} that ships the AGON
|
|
12
|
-
* "demo flow" surface: magic-link sign-in via captured mail, fixture
|
|
13
|
-
* composition for slice-by-slice extension, and stricter defaults
|
|
14
|
-
* (`fresh: true`, `mail: 'capture'`).
|
|
15
|
-
*
|
|
16
|
-
* Apps doing general browser testing should reach for `BrowserTestCase`
|
|
17
|
-
* directly; `DemoFlow` exists for the AGON slice-DoD use case.
|
|
18
|
-
*/
|
|
19
|
-
export class DemoFlow {
|
|
20
|
-
readonly tc: BrowserTestCase
|
|
21
|
-
private readonly signInEndpoint: string
|
|
22
|
-
|
|
23
|
-
constructor(tc: BrowserTestCase, options: DemoFlowOptions = {}) {
|
|
24
|
-
this.tc = tc
|
|
25
|
-
this.signInEndpoint = options.signInEndpoint ?? '/auth/magic'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Boot a DemoFlow — defaults `fresh: true`, `mail: 'capture'`. */
|
|
29
|
-
static async boot(options: DemoFlowOptions = {}): Promise<DemoFlow> {
|
|
30
|
-
const tc = await BrowserTestCase.boot({
|
|
31
|
-
fresh: options.fresh ?? true,
|
|
32
|
-
mail: options.mail ?? 'capture',
|
|
33
|
-
...options,
|
|
34
|
-
})
|
|
35
|
-
return new DemoFlow(tc, options)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Build a fixture function that returns a fresh DemoFlow with `setup`
|
|
40
|
-
* applied. Fixture flows compose: `withWorkspace` can call
|
|
41
|
-
* `signedInUser()` and extend it.
|
|
42
|
-
*/
|
|
43
|
-
static fixture<T extends DemoFlow>(setup: (flow: DemoFlow) => Promise<T>): () => Promise<T> {
|
|
44
|
-
return async () => {
|
|
45
|
-
const flow = await DemoFlow.boot()
|
|
46
|
-
return await setup(flow)
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Direct passthrough to the Playwright page for escape hatches.
|
|
51
|
-
get page(): Page { return this.tc.page }
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Delegated DSL
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
goto(path: string): Promise<void> { return this.tc.goto(path) }
|
|
58
|
-
click(selector: string): Promise<void> { return this.tc.click(selector) }
|
|
59
|
-
fill(selector: string, value: string): Promise<void> { return this.tc.fill(selector, value) }
|
|
60
|
-
expectUrl(url: string | RegExp): Promise<void> { return this.tc.expectUrl(url) }
|
|
61
|
-
expectVisible(selector: string, text?: string | RegExp): Promise<void> { return this.tc.expectVisible(selector, text) }
|
|
62
|
-
expectComputedStyle(
|
|
63
|
-
selector: string,
|
|
64
|
-
property: string,
|
|
65
|
-
matcher: Parameters<BrowserTestCase['expectComputedStyle']>[2],
|
|
66
|
-
): Promise<void> {
|
|
67
|
-
return this.tc.expectComputedStyle(selector, property, matcher)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// Auth
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Sign in via the captured magic-link flow. Posts to the configured
|
|
76
|
-
* endpoint, waits for the captured email, follows the link.
|
|
77
|
-
*/
|
|
78
|
-
signIn(opts: { email: string; endpoint?: string; subject?: string | RegExp; tokenParam?: string }): Promise<void> {
|
|
79
|
-
return this.tc.signInWithMagicLink({
|
|
80
|
-
email: opts.email,
|
|
81
|
-
endpoint: opts.endpoint ?? this.signInEndpoint,
|
|
82
|
-
subject: opts.subject,
|
|
83
|
-
tokenParam: opts.tokenParam,
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Skip the email loop — mint a session directly for `user`. */
|
|
88
|
-
signInAs(user: unknown): Promise<void> { return this.tc.signInAs(user) }
|
|
89
|
-
}
|
package/src/browser/index.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { BrowserTestCase } from './test_case.ts'
|
|
2
|
-
export type {
|
|
3
|
-
BrowserTestCaseOptions,
|
|
4
|
-
BrowserName,
|
|
5
|
-
MailMode,
|
|
6
|
-
} from './test_case.ts'
|
|
7
|
-
export { DemoFlow } from './demo_flow.ts'
|
|
8
|
-
export type { DemoFlowOptions } from './demo_flow.ts'
|
|
9
|
-
export { runFresh } from './db_fresh.ts'
|
|
10
|
-
export { startListener, stopListener } from './server_lifecycle.ts'
|
|
11
|
-
export type { ServerHandle } from './server_lifecycle.ts'
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { app, Configuration } from '@strav/kernel'
|
|
2
|
-
import { Router, Server } from '@strav/http'
|
|
3
|
-
|
|
4
|
-
export interface ServerHandle {
|
|
5
|
-
server: Server
|
|
6
|
-
router: Router
|
|
7
|
-
baseUrl: string
|
|
8
|
-
port: number
|
|
9
|
-
hostname: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Bind a Bun.serve listener for a router that has already had its routes
|
|
14
|
-
* registered. Forces an ephemeral port (port=0) unless the caller specifies
|
|
15
|
-
* one. The caller owns the lifecycle: call {@link stopListener} to tear down.
|
|
16
|
-
*/
|
|
17
|
-
export function startListener(router: Router, options: { port?: number; hostname?: string } = {}): ServerHandle {
|
|
18
|
-
const config = app.resolve(Configuration)
|
|
19
|
-
const requestedPort = options.port ?? 0
|
|
20
|
-
const requestedHost = options.hostname ?? '127.0.0.1'
|
|
21
|
-
|
|
22
|
-
// Override config so Server.start() picks up our port/host without the
|
|
23
|
-
// caller having to mutate their config files.
|
|
24
|
-
config.set('http.port', requestedPort)
|
|
25
|
-
config.set('http.host', requestedHost)
|
|
26
|
-
|
|
27
|
-
if (!app.has(Server)) app.singleton(Server)
|
|
28
|
-
const server = app.resolve(Server)
|
|
29
|
-
server.start(router)
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
server,
|
|
33
|
-
router,
|
|
34
|
-
port: server.port,
|
|
35
|
-
hostname: server.hostname,
|
|
36
|
-
baseUrl: `http://${server.hostname}:${server.port}`,
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function stopListener(handle: ServerHandle): void {
|
|
41
|
-
handle.server.stop()
|
|
42
|
-
}
|