@cosmicdrift/kumiko-testing 0.387.0 → 0.389.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -6
- package/src/changes.json +56 -0
- package/src/e2e/app-boot-budget.ts +22 -0
- package/src/e2e/auth-kit.ts +8 -2
- package/src/e2e/boundary-guard.ts +37 -13
- package/src/e2e/index.ts +11 -2
- package/src/e2e/present-identities.ts +90 -0
- package/src/e2e/screenshots.ts +6 -75
- package/src/e2e/seed-route.ts +15 -1
- package/src/e2e/seeded-tenant-fixture.ts +27 -39
- package/src/helpers/barrier.ts +5 -1
- package/src/helpers/closed-connection-error.ts +4 -7
- package/src/helpers/console-calls.ts +21 -0
- package/src/helpers/index.ts +5 -0
- package/src/helpers/mock-module.ts +24 -0
- package/src/helpers/sse-frames.ts +6 -1
- package/src/stack/db.ts +3 -3
- package/src/stack/redis.ts +7 -1
- package/src/stack/test-stack-setup.ts +628 -0
- package/src/stack/test-stack.ts +41 -524
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type ConsoleSpyLike = {
|
|
2
|
+
readonly mock: { readonly calls: ReadonlyArray<ReadonlyArray<unknown>> };
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
// Other test files in the same bun process emit async console output while a console spy is
|
|
6
|
+
// active, so assertions only count the calls that mention the message under test.
|
|
7
|
+
export function consoleCallsMentioning(
|
|
8
|
+
spy: ConsoleSpyLike,
|
|
9
|
+
fragment: string,
|
|
10
|
+
): ReadonlyArray<ReadonlyArray<unknown>> {
|
|
11
|
+
return spy.mock.calls.filter((args) => args.some((arg) => String(arg).includes(fragment)));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// For "stays silent" assertions: counts every call from the given logger namespace instead of one
|
|
15
|
+
// message text, so a new or reworded warning from the module under test still fails the test.
|
|
16
|
+
export function consoleCallsFromNamespace(
|
|
17
|
+
spy: ConsoleSpyLike,
|
|
18
|
+
namespace: string,
|
|
19
|
+
): ReadonlyArray<ReadonlyArray<unknown>> {
|
|
20
|
+
return spy.mock.calls.filter((args) => String(args[0]).startsWith(`[${namespace}]`));
|
|
21
|
+
}
|
package/src/helpers/index.ts
CHANGED
|
@@ -23,6 +23,11 @@ export {
|
|
|
23
23
|
terminateBackendsByApplicationName,
|
|
24
24
|
testDatabaseUrl,
|
|
25
25
|
} from "./closed-connection-error.ts";
|
|
26
|
+
export {
|
|
27
|
+
type ConsoleSpyLike,
|
|
28
|
+
consoleCallsFromNamespace,
|
|
29
|
+
consoleCallsMentioning,
|
|
30
|
+
} from "./console-calls.ts";
|
|
26
31
|
export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup.ts";
|
|
27
32
|
export {
|
|
28
33
|
type E2EGeneratorOptions,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { afterAll, mock } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mocks `specifier` with the real exports plus `overrides` and restores the real module in
|
|
5
|
+
* `afterAll`: bun's mock.module is process-global, so a leaked mock breaks later files of a
|
|
6
|
+
* shared DOM run. Pass the namespace imported before mocking; it is copied here because
|
|
7
|
+
* mock.module patches an already imported namespace in place. A relative specifier must be
|
|
8
|
+
* absolute (resolve it against the test file), since mock.module resolves it from this file.
|
|
9
|
+
*
|
|
10
|
+
* `restoreOverrides` fixes up modules that re-export a mocked binding: their copy was taken
|
|
11
|
+
* under the mock and holds the spy, so it must be pointed back at the real export.
|
|
12
|
+
*/
|
|
13
|
+
export function mockModuleForFile(
|
|
14
|
+
specifier: string,
|
|
15
|
+
realModule: object,
|
|
16
|
+
overrides: Readonly<Record<string, unknown>>,
|
|
17
|
+
options: { readonly restoreOverrides?: Readonly<Record<string, unknown>> } = {},
|
|
18
|
+
): void {
|
|
19
|
+
const real = { ...realModule };
|
|
20
|
+
mock.module(specifier, () => ({ ...real, ...overrides }));
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
mock.module(specifier, () => ({ ...real, ...options.restoreOverrides }));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -25,6 +25,8 @@ export async function readSseFrames(
|
|
|
25
25
|
const decoder = new TextDecoder();
|
|
26
26
|
const frames: SseFrame[] = [];
|
|
27
27
|
let buffer = "";
|
|
28
|
+
// A "\r" at a chunk end may be the first half of "\r\n", so it is held back until the next chunk.
|
|
29
|
+
let heldCarriageReturn: boolean = false;
|
|
28
30
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
29
31
|
const timedOut = new Promise<never>((_, reject) => {
|
|
30
32
|
timer = setTimeout(
|
|
@@ -40,7 +42,10 @@ export async function readSseFrames(
|
|
|
40
42
|
if (chunk.done) {
|
|
41
43
|
throw new Error(`sse stream ended before condition: ${frames.length} frames read`);
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
const decodedText = decoder.decode(chunk.value, { stream: true });
|
|
46
|
+
const decoded: string = (heldCarriageReturn ? "\r" : "") + decodedText;
|
|
47
|
+
heldCarriageReturn = decoded.endsWith("\r");
|
|
48
|
+
buffer += (heldCarriageReturn ? decoded.slice(0, -1) : decoded).replace(/\r\n?/g, "\n");
|
|
44
49
|
let separator = buffer.indexOf("\n\n");
|
|
45
50
|
while (separator !== -1) {
|
|
46
51
|
const rawFrame = buffer.slice(0, separator);
|
package/src/stack/db.ts
CHANGED
|
@@ -54,7 +54,7 @@ export function testDbTemplatePrefix(runId: string): string {
|
|
|
54
54
|
return `kumiko_test_tpl_${runId}_`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function adminUrlOf(url: string): string {
|
|
57
|
+
export function adminUrlOf(url: string): string {
|
|
58
58
|
return url.replace(/\/[^/]+$/, "/postgres");
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -99,7 +99,7 @@ export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<
|
|
|
99
99
|
db: conn.db,
|
|
100
100
|
poolMax,
|
|
101
101
|
outsideTransactionDb: outsideConn.db,
|
|
102
|
-
client: conn.
|
|
102
|
+
client: conn.listenClient,
|
|
103
103
|
dbName,
|
|
104
104
|
cleanup: async () => {
|
|
105
105
|
await conn.close();
|
|
@@ -132,7 +132,7 @@ export function ensureTestDbTemplate(
|
|
|
132
132
|
return pending;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
async function buildTestDbTemplate(
|
|
135
|
+
export async function buildTestDbTemplate(
|
|
136
136
|
templateDbName: string,
|
|
137
137
|
buildSchema: (db: DbConnection) => Promise<void>,
|
|
138
138
|
): Promise<void> {
|
package/src/stack/redis.ts
CHANGED
|
@@ -40,7 +40,13 @@ export async function createTestRedis(opts: CreateTestRedisOptions = {}): Promis
|
|
|
40
40
|
const prefix = opts.borrowKeyPrefix ?? `kt:${generateId().slice(-8)}:`;
|
|
41
41
|
// Callers that check redis.status (rate-limit resolver) must not see "connect" on their first call.
|
|
42
42
|
const redis = new Redis(redisUrl, { keyPrefix: prefix, lazyConnect: true });
|
|
43
|
-
|
|
43
|
+
try {
|
|
44
|
+
await redis.connect();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// Without this the client keeps retrying in the background and holds the process open.
|
|
47
|
+
redis.disconnect();
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
44
50
|
|
|
45
51
|
async function deleteKeysMatching(pattern: string): Promise<void> {
|
|
46
52
|
// Open a prefix-less client for the scan — ioredis' keyPrefix is applied
|