@nanobpm/urban 0.5.1 → 0.7.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.
@@ -0,0 +1,224 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import type { EngineClient, HttpRequest, HttpResponse } from "../host.ts";
4
+ import { makeRouter } from "../router.ts";
5
+ import { createPagesRoutes, type PagesDataSource } from "./pages.ts";
6
+
7
+ function req(
8
+ method: string,
9
+ path: string,
10
+ opts: { query?: string; body?: unknown } = {},
11
+ ): HttpRequest {
12
+ const bodyText = opts.body === undefined ? "" : JSON.stringify(opts.body);
13
+ return {
14
+ method,
15
+ path,
16
+ query: new URLSearchParams(opts.query ?? ""),
17
+ headers: new Headers(),
18
+ text: () => Promise.resolve(bodyText),
19
+ };
20
+ }
21
+
22
+ interface FakeEngineCalls {
23
+ created: { processDefinitionId: string; variables?: Record<string, unknown> }[];
24
+ canceled: string[];
25
+ messages: { name: string; correlationKey?: string; variables?: Record<string, unknown> }[];
26
+ }
27
+
28
+ function fakeEngine(): { engine: EngineClient; calls: FakeEngineCalls } {
29
+ const calls: FakeEngineCalls = { created: [], canceled: [], messages: [] };
30
+ const engine = {
31
+ async createInstance(input: { processDefinitionId: string; variables?: Record<string, unknown> }) {
32
+ calls.created.push(input);
33
+ return { processInstanceKey: "pi-42" };
34
+ },
35
+ async cancelInstance(input: { processInstanceKey: string }) {
36
+ calls.canceled.push(input.processInstanceKey);
37
+ },
38
+ async publishMessage(input: { name: string; correlationKey?: string; variables?: Record<string, unknown> }) {
39
+ calls.messages.push(input);
40
+ },
41
+ } as unknown as EngineClient;
42
+ return { engine, calls };
43
+ }
44
+
45
+ function fakeDb(overrides: Partial<PagesDataSource> = {}): PagesDataSource {
46
+ return {
47
+ schema: async () => [{ name: "orders" }],
48
+ query: async (sql: string) => {
49
+ if (/PRAGMA table_info/i.test(sql)) {
50
+ return [{ name: "id" }, { name: "status" }, { name: "total" }];
51
+ }
52
+ return [{ id: 1, status: "new", total: 10 }];
53
+ },
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ function build(dbOverrides: Partial<PagesDataSource> = {}) {
59
+ const { engine, calls } = fakeEngine();
60
+ const db = fakeDb(dbOverrides);
61
+ const readPage = async (path: string) => {
62
+ if (path === "pages/home.page.json") return JSON.stringify({ title: "Home", nodes: [] });
63
+ throw new Error("not found");
64
+ };
65
+ const routes = createPagesRoutes({ pagesDir: "pages", homePage: "home", sourceName: "app" }, {
66
+ db,
67
+ engine,
68
+ readPage,
69
+ });
70
+ const router = makeRouter(routes);
71
+ return { router, calls };
72
+ }
73
+
74
+ async function dispatch(method: string, path: string, opts?: { query?: string; body?: unknown }): Promise<HttpResponse> {
75
+ const { router } = build();
76
+ return router(req(method, path, opts));
77
+ }
78
+
79
+ test("GET / serves the renderer shell with the home marker", async () => {
80
+ const res = await dispatch("GET", "/");
81
+ assert.equal(res.status, 200);
82
+ assert.match(res.headers?.["content-type"] ?? "", /text\/html/);
83
+ assert.match(res.body ?? "", /data-home="home"/);
84
+ });
85
+
86
+ test("GET /app/runtime.js serves the renderer module", async () => {
87
+ const res = await dispatch("GET", "/app/runtime.js");
88
+ assert.match(res.headers?.["content-type"] ?? "", /javascript/);
89
+ assert.match(res.body ?? "", /pc:refresh/);
90
+ });
91
+
92
+ test("GET /app/pages/<id> returns the page json, 404 for unknown", async () => {
93
+ const ok = await dispatch("GET", "/app/pages/home");
94
+ assert.equal(ok.status, 200);
95
+ assert.deepEqual(JSON.parse(ok.body ?? "{}").title, "Home");
96
+ const miss = await dispatch("GET", "/app/pages/nope");
97
+ assert.equal(miss.status, 404);
98
+ });
99
+
100
+ test("GET /app/data/<source>/<table> returns rows", async () => {
101
+ const res = await dispatch("GET", "/app/data/app/orders");
102
+ assert.equal(res.status, 200);
103
+ const body = JSON.parse(res.body ?? "{}");
104
+ assert.equal(body.rows.length, 1);
105
+ assert.equal(body.rows[0].status, "new");
106
+ });
107
+
108
+ test("GET /app/data rejects unknown source, invalid + unknown table", async () => {
109
+ assert.equal((await dispatch("GET", "/app/data/other/orders")).status, 404);
110
+ const { router } = build();
111
+ const unknownTable = await router(req("GET", "/app/data/app/customers"));
112
+ assert.equal(unknownTable.status, 404);
113
+ });
114
+
115
+ test("GET /app/data whitelists filter columns", async () => {
116
+ const { router } = build();
117
+ const good = await router(req("GET", "/app/data/app/orders", { query: "where=status:new" }));
118
+ assert.equal(good.status, 200);
119
+ const bad = await router(req("GET", "/app/data/app/orders", { query: "where=evil:1" }));
120
+ assert.equal(bad.status, 400);
121
+ });
122
+
123
+ test("POST /app/actions/start/<process> creates an instance", async () => {
124
+ const { router, calls } = build();
125
+ const res = await router(req("POST", "/app/actions/start/my-proc", { body: { variables: { a: 1 } } }));
126
+ assert.equal(res.status, 200);
127
+ assert.equal(JSON.parse(res.body ?? "{}").processInstanceKey, "pi-42");
128
+ assert.equal(calls.created.length, 1);
129
+ assert.equal(calls.created[0].processDefinitionId, "my-proc");
130
+ assert.deepEqual(calls.created[0].variables, { a: 1 });
131
+ });
132
+
133
+ test("POST /app/actions/cancel cancels, 400 without a key", async () => {
134
+ const { router, calls } = build();
135
+ const ok = await router(req("POST", "/app/actions/cancel", { body: { processInstanceKey: "pi-9" } }));
136
+ assert.equal(ok.status, 200);
137
+ assert.deepEqual(calls.canceled, ["pi-9"]);
138
+ const bad = await router(req("POST", "/app/actions/cancel", { body: {} }));
139
+ assert.equal(bad.status, 400);
140
+ });
141
+
142
+ test("POST /app/actions/message publishes, 400 on missing fields", async () => {
143
+ const { router, calls } = build();
144
+ const ok = await router(req("POST", "/app/actions/message", {
145
+ body: { name: "answered", correlationKey: "k1", variables: { answer: "yes" } },
146
+ }));
147
+ assert.equal(ok.status, 200);
148
+ assert.equal(calls.messages.length, 1);
149
+ assert.equal(calls.messages[0].name, "answered");
150
+ const noName = await router(req("POST", "/app/actions/message", { body: { correlationKey: "k" } }));
151
+ assert.equal(noName.status, 400);
152
+ const noKey = await router(req("POST", "/app/actions/message", { body: { name: "m" } }));
153
+ assert.equal(noKey.status, 400);
154
+ });
155
+
156
+ test("GET /app/data quotes table/column identifiers in the emitted SQL", async () => {
157
+ const seen: string[] = [];
158
+ const db = fakeDb({
159
+ query: async (sql: string) => {
160
+ seen.push(sql);
161
+ if (/PRAGMA table_info/i.test(sql)) {
162
+ return [{ name: "id" }, { name: "status" }, { name: "total" }];
163
+ }
164
+ return [{ id: 1 }];
165
+ },
166
+ });
167
+ const routes = createPagesRoutes({ pagesDir: "pages", homePage: "home", sourceName: "app" }, {
168
+ db,
169
+ engine: fakeEngine().engine,
170
+ readPage: async () => "{}",
171
+ });
172
+ const router = makeRouter(routes);
173
+ const res = await router(req("GET", "/app/data/app/orders", {
174
+ query: "where=status:new&order=total:desc",
175
+ }));
176
+ assert.equal(res.status, 200);
177
+ const pragma = seen.find((s) => /PRAGMA table_info/i.test(s))!;
178
+ assert.match(pragma, /table_info\("orders"\)/);
179
+ const select = seen.find((s) => /^SELECT/.test(s))!;
180
+ assert.match(select, /FROM "orders"/);
181
+ assert.match(select, /WHERE "status" = \?/);
182
+ assert.match(select, /ORDER BY "total" DESC/);
183
+ });
184
+
185
+ test("GET /app/data returns a structured 500 when the SELECT query throws", async () => {
186
+ const db = fakeDb({
187
+ query: async (sql: string) => {
188
+ if (/PRAGMA table_info/i.test(sql)) return [{ name: "id" }, { name: "status" }];
189
+ throw new Error("disk gone");
190
+ },
191
+ });
192
+ const routes = createPagesRoutes({ pagesDir: "pages", homePage: "home", sourceName: "app" }, {
193
+ db,
194
+ engine: fakeEngine().engine,
195
+ readPage: async () => "{}",
196
+ });
197
+ const res = await makeRouter(routes)(req("GET", "/app/data/app/orders"));
198
+ assert.equal(res.status, 500);
199
+ assert.match(JSON.parse(res.body ?? "{}").error, /disk gone/);
200
+ });
201
+
202
+ test("rowLimit respects an explicit 0 and falls back for non-finite values", async () => {
203
+ const limits: string[] = [];
204
+ const mkDb = () => fakeDb({
205
+ query: async (sql: string) => {
206
+ if (/PRAGMA table_info/i.test(sql)) return [{ name: "id" }];
207
+ const m = sql.match(/LIMIT (\S+)/);
208
+ if (m) limits.push(m[1]);
209
+ return [];
210
+ },
211
+ });
212
+ const run = async (rowLimit: number | undefined) => {
213
+ const routes = createPagesRoutes({ pagesDir: "p", homePage: "h", sourceName: "app", rowLimit }, {
214
+ db: mkDb(),
215
+ engine: fakeEngine().engine,
216
+ readPage: async () => "{}",
217
+ });
218
+ return makeRouter(routes)(req("GET", "/app/data/app/orders"));
219
+ };
220
+ await run(0);
221
+ await run(Infinity);
222
+ await run(undefined);
223
+ assert.deepEqual(limits, ["0", "200", "200"]);
224
+ });