@owlmeans/test-integration 0.1.3

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 ADDED
@@ -0,0 +1,11 @@
1
+ # @owlmeans/test-integration
2
+
3
+ Env-gated harness for integration tests of packages that talk to external services (MongoDB, Redis, S3-compatible storage, Kubernetes). Bundles helper gates and per-run namespacing — the actual driver libraries (`mongodb`, `ioredis`, `@aws-sdk/client-s3`, `@kubernetes/client-node`) stay where they already live, in the consuming integration packages.
4
+
5
+ Helpers exported here:
6
+
7
+ - `mongoGate()`, `redisGate()`, `s3Gate()`, `kubeGate()` — read the env keys documented in `.env.example` and return `{ skip, reason?, env }`. A `tests/context.ts` uses these to decide whether to register the corresponding service in the real test context.
8
+ - `randomNamespace(prefix)` — short random suffix to keep DB names, key prefixes, and S3 prefixes unique per test run so suites can safely run in parallel.
9
+ - `registerCleanup(fn)` + `runCleanups()` — opt-in `afterAll` cleanup queue.
10
+
11
+ Specs do not call services through these helpers — they consume the per-package real test context built in `tests/context.ts` and only check `gates.<svc>.skip` to self-skip when the dependency is missing. See `testing-integration` skill for the full pattern.
@@ -0,0 +1,16 @@
1
+ type CleanupFn = () => void | Promise<void>;
2
+ /**
3
+ * Register a cleanup function that `runCleanups()` will execute (LIFO).
4
+ * Use from `tests/context.ts` `setup()` to schedule teardown of resources
5
+ * that were provisioned for the suite (DB drop, key namespace flush,
6
+ * uploaded objects).
7
+ */
8
+ export declare const registerCleanup: (fn: CleanupFn) => void;
9
+ /**
10
+ * Run all pending cleanup functions in reverse registration order.
11
+ * Errors are swallowed and reported to stderr so a failing cleanup
12
+ * cannot mask a test failure.
13
+ */
14
+ export declare const runCleanups: () => Promise<void>;
15
+ export {};
16
+ //# sourceMappingURL=cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.d.ts","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAAA,KAAK,SAAS,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAI3C;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,SAAS,KAAG,IAE/C,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,QAAa,OAAO,CAAC,IAAI,CAUhD,CAAA"}
@@ -0,0 +1,29 @@
1
+ const queue = [];
2
+ /**
3
+ * Register a cleanup function that `runCleanups()` will execute (LIFO).
4
+ * Use from `tests/context.ts` `setup()` to schedule teardown of resources
5
+ * that were provisioned for the suite (DB drop, key namespace flush,
6
+ * uploaded objects).
7
+ */
8
+ export const registerCleanup = (fn) => {
9
+ queue.push(fn);
10
+ };
11
+ /**
12
+ * Run all pending cleanup functions in reverse registration order.
13
+ * Errors are swallowed and reported to stderr so a failing cleanup
14
+ * cannot mask a test failure.
15
+ */
16
+ export const runCleanups = async () => {
17
+ while (queue.length > 0) {
18
+ const fn = queue.pop();
19
+ if (fn == null)
20
+ continue;
21
+ try {
22
+ await fn();
23
+ }
24
+ catch (err) {
25
+ console.error('@owlmeans/test-integration cleanup failed:', err);
26
+ }
27
+ }
28
+ };
29
+ //# sourceMappingURL=cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cleanup.js","sourceRoot":"","sources":["../src/cleanup.ts"],"names":[],"mappings":"AAEA,MAAM,KAAK,GAAgB,EAAE,CAAA;AAE7B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,EAAa,EAAQ,EAAE;IACrD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;AAChB,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,IAAmB,EAAE;IACnD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,KAAK,CAAC,GAAG,EAAE,CAAA;QACtB,IAAI,EAAE,IAAI,IAAI;YAAE,SAAQ;QACxB,IAAI,CAAC;YACH,MAAM,EAAE,EAAE,CAAA;QACZ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,4CAA4C,EAAE,GAAG,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;AACH,CAAC,CAAA"}
@@ -0,0 +1,29 @@
1
+ export interface IntegrationGate<E> {
2
+ skip: boolean;
3
+ reason?: string;
4
+ env: Partial<E>;
5
+ }
6
+ export interface MongoEnv {
7
+ MONGO_URL: string;
8
+ MONGO_TEST_DB_PREFIX: string;
9
+ }
10
+ export declare const mongoGate: () => IntegrationGate<MongoEnv>;
11
+ export interface RedisEnv {
12
+ REDIS_URL: string;
13
+ REDIS_TEST_KEY_PREFIX: string;
14
+ }
15
+ export declare const redisGate: () => IntegrationGate<RedisEnv>;
16
+ export interface S3Env {
17
+ S3_ENDPOINT: string;
18
+ S3_KEY: string;
19
+ S3_SECRET: string;
20
+ S3_TEST_BUCKET: string;
21
+ S3_REGION: string;
22
+ }
23
+ export declare const s3Gate: () => IntegrationGate<S3Env>;
24
+ export interface KubeEnv {
25
+ KUBE_CONFIG: string;
26
+ KUBE_TEST_OK: string;
27
+ }
28
+ export declare const kubeGate: () => IntegrationGate<KubeEnv>;
29
+ //# sourceMappingURL=gates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CAChB;AAoBD,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,oBAAoB,EAAE,MAAM,CAAA;CAC7B;AAED,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CACiB,CAAA;AAEtE,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,qBAAqB,EAAE,MAAM,CAAA;CAC9B;AAED,eAAO,MAAM,SAAS,QAAO,eAAe,CAAC,QAAQ,CACkB,CAAA;AAEvE,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,eAAO,MAAM,MAAM,QAAO,eAAe,CAAC,KAAK,CAI5C,CAAA;AAEH,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,eAAO,MAAM,QAAQ,QAAO,eAAe,CAAC,OAAO,CACU,CAAA"}
package/build/gates.js ADDED
@@ -0,0 +1,21 @@
1
+ import { hasEnv, requireEnv } from '@owlmeans/test';
2
+ const collect = (keys) => {
3
+ const env = {};
4
+ for (const k of keys) {
5
+ if (hasEnv(k))
6
+ env[k] = process.env[k];
7
+ }
8
+ return env;
9
+ };
10
+ const toIntegrationGate = (required, optional = []) => {
11
+ const gate = requireEnv(required);
12
+ const env = collect([...required, ...optional]);
13
+ if ('ok' in gate)
14
+ return { skip: false, env };
15
+ return { skip: true, reason: gate.reason, env };
16
+ };
17
+ export const mongoGate = () => toIntegrationGate(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX']);
18
+ export const redisGate = () => toIntegrationGate(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX']);
19
+ export const s3Gate = () => toIntegrationGate(['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'], ['S3_REGION']);
20
+ export const kubeGate = () => toIntegrationGate(['KUBE_CONFIG', 'KUBE_TEST_OK']);
21
+ //# sourceMappingURL=gates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gates.js","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAQnD,MAAM,OAAO,GAAG,CAAI,IAA0B,EAAc,EAAE;IAC5D,MAAM,GAAG,GAA2B,EAAE,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,MAAM,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAW,CAAA;IAClD,CAAC;IACD,OAAO,GAAiB,CAAA;AAC1B,CAAC,CAAA;AAED,MAAM,iBAAiB,GAAG,CACxB,QAA8B,EAC9B,WAAiC,EAAE,EACf,EAAE;IACtB,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IACjC,MAAM,GAAG,GAAG,OAAO,CAAI,CAAC,GAAG,QAAQ,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAA;IAClD,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;IAC7C,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAA;AACjD,CAAC,CAAA;AAOD,MAAM,CAAC,MAAM,SAAS,GAAG,GAA8B,EAAE,CACvD,iBAAiB,CAAW,CAAC,WAAW,CAAC,EAAE,CAAC,sBAAsB,CAAC,CAAC,CAAA;AAOtE,MAAM,CAAC,MAAM,SAAS,GAAG,GAA8B,EAAE,CACvD,iBAAiB,CAAW,CAAC,WAAW,CAAC,EAAE,CAAC,uBAAuB,CAAC,CAAC,CAAA;AAUvE,MAAM,CAAC,MAAM,MAAM,GAAG,GAA2B,EAAE,CACjD,iBAAiB,CACf,CAAC,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,CAAC,EACxD,CAAC,WAAW,CAAC,CACd,CAAA;AAOH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAA6B,EAAE,CACrD,iBAAiB,CAAU,CAAC,aAAa,EAAE,cAAc,CAAC,CAAC,CAAA"}
@@ -0,0 +1,4 @@
1
+ export * from './gates.js';
2
+ export * from './naming.js';
3
+ export * from './cleanup.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from './gates.js';
2
+ export * from './naming.js';
3
+ export * from './cleanup.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Short random suffix safe to use in DB names, Redis key prefixes, and S3
3
+ * object prefixes. Default 6 hex chars is enough to avoid collisions
4
+ * between parallel test runs while staying within MongoDB's 38-char DB
5
+ * name limit when combined with a sensible prefix.
6
+ */
7
+ export declare const randomNamespace: (prefix: string, length?: number) => string;
8
+ //# sourceMappingURL=naming.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.d.ts","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,QAAQ,MAAM,EAAE,SAAQ,MAAU,KAAG,MAKpE,CAAA"}
@@ -0,0 +1,14 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ /**
3
+ * Short random suffix safe to use in DB names, Redis key prefixes, and S3
4
+ * object prefixes. Default 6 hex chars is enough to avoid collisions
5
+ * between parallel test runs while staying within MongoDB's 38-char DB
6
+ * name limit when combined with a sensible prefix.
7
+ */
8
+ export const randomNamespace = (prefix, length = 6) => {
9
+ const suffix = randomBytes(Math.max(1, Math.ceil(length / 2)))
10
+ .toString('hex')
11
+ .slice(0, length);
12
+ return `${prefix}_${suffix}`;
13
+ };
14
+ //# sourceMappingURL=naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAc,EAAE,SAAiB,CAAC,EAAU,EAAE;IAC5E,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;SAC3D,QAAQ,CAAC,KAAK,CAAC;SACf,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAA;IACnB,OAAO,GAAG,MAAM,IAAI,MAAM,EAAE,CAAA;AAC9B,CAAC,CAAA"}
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@owlmeans/test-integration",
3
+ "version": "0.1.3",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsc -b",
8
+ "dev": "nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
10
+ },
11
+ "main": "build/index.js",
12
+ "module": "build/index.js",
13
+ "types": "build/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./build/index.js",
17
+ "require": "./build/index.js",
18
+ "default": "./build/index.js",
19
+ "module": "./build/index.js",
20
+ "types": "./build/index.d.ts"
21
+ }
22
+ },
23
+ "dependencies": {
24
+ "@owlmeans/test": "^0.1.3"
25
+ },
26
+ "devDependencies": {
27
+ "@owlmeans/dep-config": "^0.1.3",
28
+ "@types/bun": "^1.3.0",
29
+ "nodemon": "^3.1.11",
30
+ "typescript": "^6.0.2"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ }
35
+ }
package/src/cleanup.ts ADDED
@@ -0,0 +1,30 @@
1
+ type CleanupFn = () => void | Promise<void>
2
+
3
+ const queue: CleanupFn[] = []
4
+
5
+ /**
6
+ * Register a cleanup function that `runCleanups()` will execute (LIFO).
7
+ * Use from `tests/context.ts` `setup()` to schedule teardown of resources
8
+ * that were provisioned for the suite (DB drop, key namespace flush,
9
+ * uploaded objects).
10
+ */
11
+ export const registerCleanup = (fn: CleanupFn): void => {
12
+ queue.push(fn)
13
+ }
14
+
15
+ /**
16
+ * Run all pending cleanup functions in reverse registration order.
17
+ * Errors are swallowed and reported to stderr so a failing cleanup
18
+ * cannot mask a test failure.
19
+ */
20
+ export const runCleanups = async (): Promise<void> => {
21
+ while (queue.length > 0) {
22
+ const fn = queue.pop()
23
+ if (fn == null) continue
24
+ try {
25
+ await fn()
26
+ } catch (err) {
27
+ console.error('@owlmeans/test-integration cleanup failed:', err)
28
+ }
29
+ }
30
+ }
package/src/gates.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { hasEnv, requireEnv } from '@owlmeans/test'
2
+
3
+ export interface IntegrationGate<E> {
4
+ skip: boolean
5
+ reason?: string
6
+ env: Partial<E>
7
+ }
8
+
9
+ const collect = <E>(keys: (keyof E & string)[]): Partial<E> => {
10
+ const env: Record<string, string> = {}
11
+ for (const k of keys) {
12
+ if (hasEnv(k)) env[k] = process.env[k] as string
13
+ }
14
+ return env as Partial<E>
15
+ }
16
+
17
+ const toIntegrationGate = <E>(
18
+ required: (keyof E & string)[],
19
+ optional: (keyof E & string)[] = []
20
+ ): IntegrationGate<E> => {
21
+ const gate = requireEnv(required)
22
+ const env = collect<E>([...required, ...optional])
23
+ if ('ok' in gate) return { skip: false, env }
24
+ return { skip: true, reason: gate.reason, env }
25
+ }
26
+
27
+ export interface MongoEnv {
28
+ MONGO_URL: string
29
+ MONGO_TEST_DB_PREFIX: string
30
+ }
31
+
32
+ export const mongoGate = (): IntegrationGate<MongoEnv> =>
33
+ toIntegrationGate<MongoEnv>(['MONGO_URL'], ['MONGO_TEST_DB_PREFIX'])
34
+
35
+ export interface RedisEnv {
36
+ REDIS_URL: string
37
+ REDIS_TEST_KEY_PREFIX: string
38
+ }
39
+
40
+ export const redisGate = (): IntegrationGate<RedisEnv> =>
41
+ toIntegrationGate<RedisEnv>(['REDIS_URL'], ['REDIS_TEST_KEY_PREFIX'])
42
+
43
+ export interface S3Env {
44
+ S3_ENDPOINT: string
45
+ S3_KEY: string
46
+ S3_SECRET: string
47
+ S3_TEST_BUCKET: string
48
+ S3_REGION: string
49
+ }
50
+
51
+ export const s3Gate = (): IntegrationGate<S3Env> =>
52
+ toIntegrationGate<S3Env>(
53
+ ['S3_ENDPOINT', 'S3_KEY', 'S3_SECRET', 'S3_TEST_BUCKET'],
54
+ ['S3_REGION']
55
+ )
56
+
57
+ export interface KubeEnv {
58
+ KUBE_CONFIG: string
59
+ KUBE_TEST_OK: string
60
+ }
61
+
62
+ export const kubeGate = (): IntegrationGate<KubeEnv> =>
63
+ toIntegrationGate<KubeEnv>(['KUBE_CONFIG', 'KUBE_TEST_OK'])
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './gates.js'
2
+ export * from './naming.js'
3
+ export * from './cleanup.js'
package/src/naming.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { randomBytes } from 'node:crypto'
2
+
3
+ /**
4
+ * Short random suffix safe to use in DB names, Redis key prefixes, and S3
5
+ * object prefixes. Default 6 hex chars is enough to avoid collisions
6
+ * between parallel test runs while staying within MongoDB's 38-char DB
7
+ * name limit when combined with a sensible prefix.
8
+ */
9
+ export const randomNamespace = (prefix: string, length: number = 6): string => {
10
+ const suffix = randomBytes(Math.max(1, Math.ceil(length / 2)))
11
+ .toString('hex')
12
+ .slice(0, length)
13
+ return `${prefix}_${suffix}`
14
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": [
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.bun.json"
5
+ ],
6
+ "compilerOptions": {
7
+ "rootDir": "./src/",
8
+ "outDir": "./build/"
9
+ },
10
+ "exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
11
+ }