@arcote.tech/arc-testing 0.7.27

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.
Files changed (3) hide show
  1. package/README.md +94 -0
  2. package/dist/index.js +121 -0
  3. package/package.json +37 -0
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @arcote.tech/arc-testing
2
+
3
+ In-process test harness for Arc. Write **use-case-shaped** tests on `bun:test`
4
+ using the **same scope API the client uses** — no port, no mocks of the framework,
5
+ full type inference from your context.
6
+
7
+ ```ts
8
+ import { createHarness } from "@arcote.tech/arc-testing";
9
+
10
+ const h = await createHarness(myContext);
11
+
12
+ // declare current state
13
+ await h.seed.rows("plan", [{ _id: "plan_pro", price: 4900 }]);
14
+
15
+ // an authenticated actor — the SAME scope API as the app
16
+ const creator = h.scope(userToken, { projectId });
17
+
18
+ await creator.mutation().startCheckout({ projectId, planId: "plan_pro" });
19
+ const order = await creator.query().paymentOrder.findOne({ _id: orderId });
20
+
21
+ // a webhook / route, in-process (real route boundary, no port)
22
+ await h.route("payuWebhook", { orderId, status: "COMPLETED" });
23
+
24
+ // drain the async listener cascade deterministically (no setTimeout)
25
+ await h.settle();
26
+
27
+ // assert events + state with native bun:test expect
28
+ expect(h.events.ofType("subscriptionActivated")).toHaveLength(1);
29
+ expect(await creator.query().subscription.findOne({ _id: projectId }))
30
+ .toMatchObject({ status: "active" });
31
+ ```
32
+
33
+ ## API
34
+
35
+ | | |
36
+ |---|---|
37
+ | `createHarness(context)` | builds the harness (in-memory SQLite, declarative seeds run, listeners active) |
38
+ | `h.scope(token, params)` | an authenticated scope — `.mutation()` (= client `useMutation`), `.query()` (= client `useQuery`, awaited rows) |
39
+ | `h.anonymous()` | a scope with no identity (public caller) |
40
+ | `h.seed.rows(store, rows)` | raw insert into a view/aggregate store (like `withSeed`); rows need `_id` |
41
+ | `h.seed.event(type, payload, { as })` | publish through real projection handlers (and listeners) |
42
+ | `h.route(name, body, { headers, token })` | invoke a route/webhook in-process (real verification + protection gate) |
43
+ | `h.settle()` | resolve once the async listener cascade has fully drained |
44
+ | `h.events` | typed log: `.ofType(name)`, `.last(name)`, `.count(name)`, `.all()` |
45
+
46
+ `mutation()` / `query()` are the exact client `ScopeAPI` accessors (core's
47
+ `MutationExecutor` / `QueryAccessor`) — element names, params, payloads and rows
48
+ are all inferred from your context. No new naming layer, no `any`.
49
+
50
+ ## Recipe: testing the real billing `buy-subscription-card` (platform)
51
+
52
+ Prerequisites (one-time, user-gated):
53
+ 1. Publish `@arcote.tech/arc`, `@arcote.tech/arc-host`, `@arcote.tech/arc-testing` to npm.
54
+ 2. Add `@arcote.tech/arc-testing` to `packages/billing` devDependencies (npm, **not** `link:` — `link:` breaks the Docker build).
55
+
56
+ PayU-free reaction test (avoids the external gateway by injecting the domain
57
+ event, then asserting the **real** billing listener cascade — grant → activation):
58
+
59
+ ```ts
60
+ import { describe, it, expect, beforeEach } from "bun:test";
61
+ import { createHarness, type Harness } from "@arcote.tech/arc-testing";
62
+ import { getBillingContext } from "@ndt/billing";
63
+ import { userToken } from "@ndt/auth";
64
+
65
+ describe("buy-subscription-card (billing reaction)", () => {
66
+ let h: Harness<ReturnType<typeof getBillingContext>>;
67
+ const accountId = "acc_test";
68
+
69
+ beforeEach(async () => { h = await createHarness(getBillingContext()); });
70
+
71
+ it("paid order → grant → subscription activated", async () => {
72
+ const user = h.scope(userToken, { accountId, role: "user" });
73
+
74
+ // inject the 'payment completed' domain event with the caller's identity,
75
+ // then let the real billingPaymentGrant → billingSubscriptionActivation chain run
76
+ await h.seed.event(
77
+ "paymentOrderCompleted",
78
+ { _id: "order_1", userId: accountId, amountPLNGrosz: 4900, kind: "subscription" },
79
+ { as: { tokenName: "user", params: { accountId, role: "user" } } },
80
+ );
81
+ await h.settle();
82
+
83
+ expect(h.events.ofType("subscriptionActivated")).toHaveLength(1);
84
+ const sub = await user.query().subscription.findOne({ _id: accountId });
85
+ expect(sub).toMatchObject({ /* active subscription shape */ });
86
+ });
87
+ });
88
+ ```
89
+
90
+ For the **full** checkout-through-webhook flow (`startCheckoutCardAsUser` →
91
+ `payuWebhook`), stub the PayU client the checkout/route modules call, then drive
92
+ it with `creator.mutation().startCheckoutCardAsUser(...)` and
93
+ `h.route("payuWebhook", payuBody)` exactly as the self-proof does
94
+ (`tests/buy-subscription-like.test.ts`).
package/dist/index.js ADDED
@@ -0,0 +1,121 @@
1
+ // src/harness.ts
2
+ import {
3
+ BunSQLiteDatabase,
4
+ createSQLiteAdapterFactory
5
+ } from "@arcote.tech/arc-adapter-db-sqlite";
6
+ import { ContextHandler } from "@arcote.tech/arc-host";
7
+
8
+ // src/events.ts
9
+ function createEventLog(publisher) {
10
+ const records = [];
11
+ publisher.subscribe("*", (ev) => {
12
+ records.push({
13
+ id: ev.id,
14
+ type: ev.type,
15
+ payload: ev.payload,
16
+ createdAt: ev.createdAt ?? new Date,
17
+ authContext: ev.authContext ?? null
18
+ });
19
+ });
20
+ const matches = (recordType, query) => recordType === query || recordType.endsWith(`.${query}`);
21
+ return {
22
+ all: () => [...records],
23
+ ofType: (type) => records.filter((r) => matches(r.type, type)),
24
+ last: (type) => {
25
+ for (let i = records.length - 1;i >= 0; i--) {
26
+ if (matches(records[i].type, type))
27
+ return records[i];
28
+ }
29
+ return;
30
+ },
31
+ count: (type) => records.filter((r) => matches(r.type, type)).length
32
+ };
33
+ }
34
+
35
+ // src/scope.ts
36
+ import {
37
+ buildContextAccessor,
38
+ ScopedModel
39
+ } from "@arcote.tech/arc";
40
+ function createScope(model, name) {
41
+ const scoped = new ScopedModel(model, name);
42
+ const mutation = () => buildContextAccessor(scoped.context, scoped.getAdapters(), "mutateContext", (_descriptor, execute) => execute());
43
+ const query = () => buildContextAccessor(scoped.context, scoped.getAdapters(), "queryContext", (_descriptor, execute) => execute());
44
+ return {
45
+ mutation,
46
+ query,
47
+ setToken: (token) => scoped.setToken(token),
48
+ setIdentity: (tokenName, params) => {
49
+ scoped.getAdapters().authAdapter?.setDecoded({ tokenName, params });
50
+ },
51
+ scoped
52
+ };
53
+ }
54
+
55
+ // src/seed.ts
56
+ function createSeed(dataStorage, publisher) {
57
+ let counter = 0;
58
+ return {
59
+ rows: async (store, rows) => {
60
+ const target = dataStorage.getStore(store);
61
+ await target.applyChanges(rows.map((data) => ({ type: "set", data })));
62
+ },
63
+ event: async (type, payload, opts) => {
64
+ await publisher.publish({
65
+ id: `seed_${type}_${++counter}`,
66
+ type,
67
+ payload,
68
+ createdAt: new Date,
69
+ authContext: opts?.as ?? null
70
+ });
71
+ }
72
+ };
73
+ }
74
+
75
+ // src/harness.ts
76
+ var scopeCounter = 0;
77
+ async function createHarness(context) {
78
+ const factory = createSQLiteAdapterFactory(new BunSQLiteDatabase(":memory:"));
79
+ const contextHandler = new ContextHandler(context, factory(context));
80
+ await contextHandler.init();
81
+ const model = contextHandler.getModel();
82
+ const dataStorage = contextHandler.getDataStorage();
83
+ const publisher = contextHandler.getEventPublisher();
84
+ const events = createEventLog(publisher);
85
+ const seed = createSeed(dataStorage, publisher);
86
+ function scope(tokenOrName, params) {
87
+ if (typeof tokenOrName === "string") {
88
+ return createScope(model, tokenOrName);
89
+ }
90
+ const s = createScope(model, `${tokenOrName.name}_${++scopeCounter}`);
91
+ s.setIdentity(tokenOrName.name, params ?? {});
92
+ return s;
93
+ }
94
+ return {
95
+ scope,
96
+ anonymous: () => createScope(model, `anon_${++scopeCounter}`),
97
+ seed,
98
+ events,
99
+ route: async (name, body, opts) => {
100
+ const response = await contextHandler.executeRoute(name, {
101
+ method: opts?.method,
102
+ body,
103
+ headers: opts?.headers,
104
+ query: opts?.query
105
+ }, opts?.token ?? null);
106
+ let json = null;
107
+ try {
108
+ json = await response.clone().json();
109
+ } catch {
110
+ json = null;
111
+ }
112
+ return { status: response.status, json, response };
113
+ },
114
+ settle: () => publisher.whenIdle?.() ?? Promise.resolve(),
115
+ destroy: () => dataStorage.destroy(),
116
+ contextHandler
117
+ };
118
+ }
119
+ export {
120
+ createHarness
121
+ };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@arcote.tech/arc-testing",
3
+ "version": "0.7.27",
4
+ "type": "module",
5
+ "private": false,
6
+ "author": "Przemysław Krasiński [arcote.tech]",
7
+ "description": "In-process test harness for Arc — write use-case-shaped tests on bun:test with the same scope API the client uses",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "rm -rf dist && bun build ./src/index.ts --outdir ./dist --target node --format esm --external @arcote.tech/arc --external @arcote.tech/arc-host --external @arcote.tech/arc-adapter-db-sqlite && bun run build:types",
23
+ "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir ./dist",
24
+ "postbuild": "rm -f tsconfig.tsbuildinfo",
25
+ "type-check": "tsc --noEmit",
26
+ "test": "bun test"
27
+ },
28
+ "peerDependencies": {
29
+ "@arcote.tech/arc": "^0.7.27",
30
+ "@arcote.tech/arc-host": "^0.7.27",
31
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.27"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "latest",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }