sermon-js 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +239 -2
- package/dist/api/index.cjs +174 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +77 -0
- package/dist/api/index.d.ts +77 -0
- package/dist/api/index.js +171 -0
- package/dist/api/index.js.map +1 -0
- package/dist/chunk-2M4UBCDM.js +178 -0
- package/dist/chunk-2M4UBCDM.js.map +1 -0
- package/dist/chunk-AH372HLN.js +98 -0
- package/dist/chunk-AH372HLN.js.map +1 -0
- package/dist/chunk-APC2HVSI.js +154 -0
- package/dist/chunk-APC2HVSI.js.map +1 -0
- package/dist/chunk-D7BBJTYU.cjs +335 -0
- package/dist/chunk-D7BBJTYU.cjs.map +1 -0
- package/dist/chunk-ELV4ZU6G.cjs +51 -0
- package/dist/chunk-ELV4ZU6G.cjs.map +1 -0
- package/dist/chunk-EMP7XVKC.js +334 -0
- package/dist/chunk-EMP7XVKC.js.map +1 -0
- package/dist/chunk-I3346MUL.js +49 -0
- package/dist/chunk-I3346MUL.js.map +1 -0
- package/dist/chunk-NP3YYVRZ.cjs +163 -0
- package/dist/chunk-NP3YYVRZ.cjs.map +1 -0
- package/dist/chunk-PARE6EUZ.cjs +184 -0
- package/dist/chunk-PARE6EUZ.cjs.map +1 -0
- package/dist/chunk-XHXEKB3Q.cjs +241 -0
- package/dist/chunk-XHXEKB3Q.cjs.map +1 -0
- package/dist/chunk-XPXIDJUU.js +229 -0
- package/dist/chunk-XPXIDJUU.js.map +1 -0
- package/dist/chunk-ZI442ZA5.cjs +100 -0
- package/dist/chunk-ZI442ZA5.cjs.map +1 -0
- package/dist/config-BEbP-uLf.d.cts +125 -0
- package/dist/config-w1tm1bdu.d.ts +125 -0
- package/dist/generic-BJmlgtUM.d.ts +31 -0
- package/dist/generic-lmrGxMDC.d.cts +31 -0
- package/dist/index.cjs +407 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +93 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +330 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/index.cjs +1012 -0
- package/dist/presets/index.cjs.map +1 -0
- package/dist/presets/index.d.cts +516 -0
- package/dist/presets/index.d.ts +516 -0
- package/dist/presets/index.js +971 -0
- package/dist/presets/index.js.map +1 -0
- package/dist/react/heroui/index.cjs +822 -0
- package/dist/react/heroui/index.cjs.map +1 -0
- package/dist/react/heroui/index.d.cts +122 -0
- package/dist/react/heroui/index.d.ts +122 -0
- package/dist/react/heroui/index.js +821 -0
- package/dist/react/heroui/index.js.map +1 -0
- package/dist/react/heroui/styles.css +11 -0
- package/dist/react/index.cjs +71 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +53 -0
- package/dist/react/index.d.ts +53 -0
- package/dist/react/index.js +70 -0
- package/dist/react/index.js.map +1 -0
- package/dist/status-DsWzgb9Q.d.cts +146 -0
- package/dist/status-zd04ZQ98.d.ts +146 -0
- package/dist/store-DybhDphS.d.cts +77 -0
- package/dist/store-_GuzSP0z.d.ts +77 -0
- package/dist/stores/mongodb.cjs +99 -0
- package/dist/stores/mongodb.cjs.map +1 -0
- package/dist/stores/mongodb.d.cts +40 -0
- package/dist/stores/mongodb.d.ts +40 -0
- package/dist/stores/mongodb.js +97 -0
- package/dist/stores/mongodb.js.map +1 -0
- package/dist/stores/redis.cjs +23 -0
- package/dist/stores/redis.cjs.map +1 -0
- package/dist/stores/redis.d.cts +31 -0
- package/dist/stores/redis.d.ts +31 -0
- package/dist/stores/redis.js +21 -0
- package/dist/stores/redis.js.map +1 -0
- package/dist/stores/sql.cjs +164 -0
- package/dist/stores/sql.cjs.map +1 -0
- package/dist/stores/sql.d.cts +39 -0
- package/dist/stores/sql.d.ts +39 -0
- package/dist/stores/sql.js +162 -0
- package/dist/stores/sql.js.map +1 -0
- package/dist/stores/upstash.cjs +27 -0
- package/dist/stores/upstash.cjs.map +1 -0
- package/dist/stores/upstash.d.cts +19 -0
- package/dist/stores/upstash.d.ts +19 -0
- package/dist/stores/upstash.js +25 -0
- package/dist/stores/upstash.js.map +1 -0
- package/dist/testing/index.cjs +209 -0
- package/dist/testing/index.cjs.map +1 -0
- package/dist/testing/index.d.cts +66 -0
- package/dist/testing/index.d.ts +66 -0
- package/dist/testing/index.js +205 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tree-BXbXfjF1.d.cts +236 -0
- package/dist/tree-BXbXfjF1.d.ts +236 -0
- package/dist/types-CctvUAs2.d.cts +192 -0
- package/dist/types-CctvUAs2.d.ts +192 -0
- package/package.json +233 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { S as StatusStore } from '../store-_GuzSP0z.js';
|
|
2
|
+
import '../types-CctvUAs2.js';
|
|
3
|
+
|
|
4
|
+
type UpstashStoreOptions = {
|
|
5
|
+
/** REST URL of the database (`UPSTASH_REDIS_REST_URL` / Vercel KV's `KV_REST_API_URL`) */
|
|
6
|
+
url: string;
|
|
7
|
+
/** REST token (`UPSTASH_REDIS_REST_TOKEN` / `KV_REST_API_TOKEN`) */
|
|
8
|
+
token: string;
|
|
9
|
+
/** Prefix of every key (default "sermon"): lets several status pages share one database */
|
|
10
|
+
prefix?: string;
|
|
11
|
+
fetch?: typeof fetch;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Store on Upstash Redis or Vercel KV, over their REST API. It only needs `fetch`, so it works
|
|
15
|
+
* on serverless and edge runtimes, with no client library to install.
|
|
16
|
+
*/
|
|
17
|
+
declare function upstashStore({ url, token, prefix, fetch: fetchImpl }: UpstashStoreOptions): StatusStore;
|
|
18
|
+
|
|
19
|
+
export { type UpstashStoreOptions, upstashStore };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createRedisStore } from '../chunk-AH372HLN.js';
|
|
2
|
+
|
|
3
|
+
// src/stores/upstash.ts
|
|
4
|
+
function upstashStore({ url, token, prefix, fetch: fetchImpl = fetch }) {
|
|
5
|
+
if (!url || !token) throw new Error("sermon-js: upstashStore needs a url and a token");
|
|
6
|
+
const endpoint = `${url.replace(/\/+$/, "")}/pipeline`;
|
|
7
|
+
const exec = async (commands) => {
|
|
8
|
+
const response = await fetchImpl(endpoint, {
|
|
9
|
+
method: "POST",
|
|
10
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
11
|
+
body: JSON.stringify(commands.map((command) => command.map(String)))
|
|
12
|
+
});
|
|
13
|
+
if (!response.ok) throw new Error(`sermon-js: Upstash request failed (HTTP ${response.status})`);
|
|
14
|
+
const replies = await response.json();
|
|
15
|
+
return replies.map((reply) => {
|
|
16
|
+
if (reply.error) throw new Error(`sermon-js: Upstash command failed: ${reply.error}`);
|
|
17
|
+
return reply.result;
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
return createRedisStore({ exec, prefix });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { upstashStore };
|
|
24
|
+
//# sourceMappingURL=upstash.js.map
|
|
25
|
+
//# sourceMappingURL=upstash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/stores/upstash.ts"],"names":[],"mappings":";;;AAiBO,SAAS,YAAA,CAAa,EAAE,GAAA,EAAK,KAAA,EAAO,QAAQ,KAAA,EAAO,SAAA,GAAY,OAAM,EAAqC;AAC/G,EAAA,IAAI,CAAC,GAAA,IAAO,CAAC,OAAO,MAAM,IAAI,MAAM,iDAAiD,CAAA;AACrF,EAAA,MAAM,WAAW,CAAA,EAAG,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA,SAAA,CAAA;AAE3C,EAAA,MAAM,IAAA,GAAkB,OAAO,QAAA,KAAa;AAC1C,IAAA,MAAM,QAAA,GAAW,MAAM,SAAA,CAAU,QAAA,EAAU;AAAA,MACzC,MAAA,EAAQ,MAAA;AAAA,MACR,SAAS,EAAE,aAAA,EAAe,UAAU,KAAK,CAAA,CAAA,EAAI,gBAAgB,kBAAA,EAAmB;AAAA,MAChF,IAAA,EAAM,IAAA,CAAK,SAAA,CAAU,QAAA,CAAS,GAAA,CAAI,CAAC,OAAA,KAAY,OAAA,CAAQ,GAAA,CAAI,MAAM,CAAC,CAAC;AAAA,KACpE,CAAA;AACD,IAAA,IAAI,CAAC,SAAS,EAAA,EAAI,MAAM,IAAI,KAAA,CAAM,CAAA,wCAAA,EAA2C,QAAA,CAAS,MAAM,CAAA,CAAA,CAAG,CAAA;AAC/F,IAAA,MAAM,OAAA,GAAW,MAAM,QAAA,CAAS,IAAA,EAAK;AACrC,IAAA,OAAO,OAAA,CAAQ,GAAA,CAAI,CAAC,KAAA,KAAU;AAC5B,MAAA,IAAI,KAAA,CAAM,OAAO,MAAM,IAAI,MAAM,CAAA,mCAAA,EAAsC,KAAA,CAAM,KAAK,CAAA,CAAE,CAAA;AACpF,MAAA,OAAO,KAAA,CAAM,MAAA;AAAA,IACf,CAAC,CAAA;AAAA,EACH,CAAA;AAEA,EAAA,OAAO,gBAAA,CAAiB,EAAE,IAAA,EAAM,MAAA,EAAQ,CAAA;AAC1C","file":"upstash.js","sourcesContent":["import type { StatusStore } from \"../store\";\nimport { createRedisStore, type RedisExec } from \"./redis-core\";\n\nexport type UpstashStoreOptions = {\n /** REST URL of the database (`UPSTASH_REDIS_REST_URL` / Vercel KV's `KV_REST_API_URL`) */\n url: string;\n /** REST token (`UPSTASH_REDIS_REST_TOKEN` / `KV_REST_API_TOKEN`) */\n token: string;\n /** Prefix of every key (default \"sermon\"): lets several status pages share one database */\n prefix?: string;\n fetch?: typeof fetch;\n};\n\n/**\n * Store on Upstash Redis or Vercel KV, over their REST API. It only needs `fetch`, so it works\n * on serverless and edge runtimes, with no client library to install.\n */\nexport function upstashStore({ url, token, prefix, fetch: fetchImpl = fetch }: UpstashStoreOptions): StatusStore {\n if (!url || !token) throw new Error(\"sermon-js: upstashStore needs a url and a token\");\n const endpoint = `${url.replace(/\\/+$/, \"\")}/pipeline`;\n\n const exec: RedisExec = async (commands) => {\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { authorization: `Bearer ${token}`, \"content-type\": \"application/json\" },\n body: JSON.stringify(commands.map((command) => command.map(String))),\n });\n if (!response.ok) throw new Error(`sermon-js: Upstash request failed (HTTP ${response.status})`);\n const replies = (await response.json()) as Array<{ result?: unknown; error?: string }>;\n return replies.map((reply) => {\n if (reply.error) throw new Error(`sermon-js: Upstash command failed: ${reply.error}`);\n return reply.result;\n });\n };\n\n return createRedisStore({ exec, prefix });\n}\n"]}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkPARE6EUZ_cjs = require('../chunk-PARE6EUZ.cjs');
|
|
4
|
+
var chunkXHXEKB3Q_cjs = require('../chunk-XHXEKB3Q.cjs');
|
|
5
|
+
|
|
6
|
+
// src/testing/index.ts
|
|
7
|
+
var summary = (overrides = {}) => ({
|
|
8
|
+
at: 1e3,
|
|
9
|
+
trigger: "scheduled",
|
|
10
|
+
full: true,
|
|
11
|
+
score: 100,
|
|
12
|
+
label: "operational",
|
|
13
|
+
failing: {},
|
|
14
|
+
...overrides
|
|
15
|
+
});
|
|
16
|
+
var result = (overrides = {}) => ({
|
|
17
|
+
key: "api",
|
|
18
|
+
status: "up",
|
|
19
|
+
checkedAt: 1e3,
|
|
20
|
+
probed: true,
|
|
21
|
+
latencyMs: 120,
|
|
22
|
+
...overrides
|
|
23
|
+
});
|
|
24
|
+
var sample = (overrides = {}) => ({
|
|
25
|
+
key: "api",
|
|
26
|
+
at: 1e3,
|
|
27
|
+
status: "up",
|
|
28
|
+
latencyMs: 120,
|
|
29
|
+
...overrides
|
|
30
|
+
});
|
|
31
|
+
var clean = (value) => JSON.parse(JSON.stringify(value));
|
|
32
|
+
function resolveHarness(harness) {
|
|
33
|
+
if (harness) return harness;
|
|
34
|
+
const globals = globalThis;
|
|
35
|
+
if (globals.describe && globals.it && globals.expect) {
|
|
36
|
+
return { describe: globals.describe, it: globals.it, expect: globals.expect };
|
|
37
|
+
}
|
|
38
|
+
throw new Error(
|
|
39
|
+
"sermon-js/testing: pass your test runner's { describe, it, expect } as the third argument (or enable the runner's globals)."
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
function runStoreConformance(name, createStore, harness) {
|
|
43
|
+
const { describe, it, expect } = resolveHarness(harness);
|
|
44
|
+
describe(`${name}: StatusStore conformance`, () => {
|
|
45
|
+
it("starts empty", async () => {
|
|
46
|
+
const store = await createStore();
|
|
47
|
+
expect(await store.latest()).toEqual({});
|
|
48
|
+
expect(await store.runs({ since: 0, until: 1e4 })).toEqual([]);
|
|
49
|
+
expect(await store.samples("api", { since: 0, until: 1e4 })).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
it("keeps the latest result of each service, with all its fields", async () => {
|
|
52
|
+
const store = await createStore();
|
|
53
|
+
const rich = result({
|
|
54
|
+
key: "fly",
|
|
55
|
+
status: "degraded",
|
|
56
|
+
httpStatus: 200,
|
|
57
|
+
error: "1/2 machine(s) down: sin",
|
|
58
|
+
metrics: { machines: "1/2 running", count: 2 },
|
|
59
|
+
usage: [{ label: "Requests", used: 15, limit: 100, unit: "requests", resetsAt: 9999 }],
|
|
60
|
+
instances: [
|
|
61
|
+
{ key: "ams", region: "ams", id: "m1", state: "started", status: "up", latencyMs: 80, httpStatus: 200 },
|
|
62
|
+
{ key: "sin", region: "sin", id: "m2", state: "stopped", status: "down", error: "Out of memory" }
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
await store.saveRun({
|
|
66
|
+
summary: summary(),
|
|
67
|
+
results: [result(), rich, result({ key: "off", status: "unconfigured", probed: false, latencyMs: void 0 })],
|
|
68
|
+
samples: [sample(), sample({ key: "fly", status: "degraded" })]
|
|
69
|
+
});
|
|
70
|
+
await store.saveRun({
|
|
71
|
+
summary: summary({ at: 2e3, full: false }),
|
|
72
|
+
results: [result({ checkedAt: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })],
|
|
73
|
+
samples: [sample({ at: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })]
|
|
74
|
+
});
|
|
75
|
+
const latest = await store.latest();
|
|
76
|
+
expect(Object.keys(latest).sort()).toEqual(["api", "fly", "off"]);
|
|
77
|
+
expect(clean(latest.api)).toEqual(clean(result({ checkedAt: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })));
|
|
78
|
+
expect(clean(latest.fly)).toEqual(clean(rich));
|
|
79
|
+
expect(latest.off?.status).toBe("unconfigured");
|
|
80
|
+
});
|
|
81
|
+
it("returns runs within an inclusive range, oldest first", async () => {
|
|
82
|
+
const store = await createStore();
|
|
83
|
+
for (const at of [3e3, 1e3, 2e3]) {
|
|
84
|
+
await store.saveRun({
|
|
85
|
+
summary: summary({ at, score: at / 100, failing: at === 2e3 ? { api: "down" } : {} }),
|
|
86
|
+
results: [],
|
|
87
|
+
samples: []
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const runs = await store.runs({ since: 1e3, until: 2e3 });
|
|
91
|
+
expect(runs.map((run) => run.at)).toEqual([1e3, 2e3]);
|
|
92
|
+
expect(clean(runs[1])).toEqual(
|
|
93
|
+
clean(summary({ at: 2e3, score: 20, failing: { api: "down" } }))
|
|
94
|
+
);
|
|
95
|
+
expect((await store.runs({ since: 0, until: 9e3 })).map((run) => run.at)).toEqual([1e3, 2e3, 3e3]);
|
|
96
|
+
});
|
|
97
|
+
it("returns samples of one service within an inclusive range, oldest first", async () => {
|
|
98
|
+
const store = await createStore();
|
|
99
|
+
await store.saveRun({
|
|
100
|
+
summary: summary({ at: 2e3 }),
|
|
101
|
+
results: [],
|
|
102
|
+
samples: [
|
|
103
|
+
sample({ at: 2e3, instances: { ams: ["up", 80], sin: ["sleeping"] } }),
|
|
104
|
+
sample({ key: "other", at: 2e3 })
|
|
105
|
+
]
|
|
106
|
+
});
|
|
107
|
+
await store.saveRun({ summary: summary({ at: 1e3 }), results: [], samples: [sample({ at: 1e3 })] });
|
|
108
|
+
await store.saveRun({ summary: summary({ at: 3e3 }), results: [], samples: [sample({ at: 3e3 })] });
|
|
109
|
+
const samples = await store.samples("api", { since: 1e3, until: 2e3 });
|
|
110
|
+
expect(samples.map((s) => s.at)).toEqual([1e3, 2e3]);
|
|
111
|
+
expect(samples.every((s) => s.key === "api")).toBe(true);
|
|
112
|
+
expect(clean(samples[1]?.instances)).toEqual({ ams: ["up", 80], sin: ["sleeping"] });
|
|
113
|
+
});
|
|
114
|
+
it("prunes old runs and samples", async () => {
|
|
115
|
+
const store = await createStore();
|
|
116
|
+
for (const at of [1e3, 2e3, 3e3]) {
|
|
117
|
+
await store.saveRun({ summary: summary({ at }), results: [result({ checkedAt: at })], samples: [sample({ at })] });
|
|
118
|
+
}
|
|
119
|
+
await store.prune({ before: 2e3 });
|
|
120
|
+
expect((await store.runs({ since: 0, until: 9e3 })).map((run) => run.at)).toEqual([2e3, 3e3]);
|
|
121
|
+
expect((await store.samples("api", { since: 0, until: 9e3 })).map((s) => s.at)).toEqual([2e3, 3e3]);
|
|
122
|
+
expect(Object.keys(await store.latest())).toEqual(["api"]);
|
|
123
|
+
});
|
|
124
|
+
it("drops services that are not in keepKeys", async () => {
|
|
125
|
+
const store = await createStore();
|
|
126
|
+
await store.saveRun({
|
|
127
|
+
summary: summary(),
|
|
128
|
+
results: [result(), result({ key: "removed" })],
|
|
129
|
+
samples: [sample(), sample({ key: "removed" })]
|
|
130
|
+
});
|
|
131
|
+
await store.prune({ before: 0, keepKeys: ["api"] });
|
|
132
|
+
expect(Object.keys(await store.latest())).toEqual(["api"]);
|
|
133
|
+
expect(await store.samples("removed", { since: 0, until: 9e3 })).toEqual([]);
|
|
134
|
+
expect(await store.samples("api", { since: 0, until: 9e3 })).toHaveLength(1);
|
|
135
|
+
expect(await store.runs({ since: 0, until: 9e3 })).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
it("locks by name until the time-to-live passes (when supported)", async () => {
|
|
138
|
+
const store = await createStore();
|
|
139
|
+
if (!store.lock) return;
|
|
140
|
+
expect(await store.lock("run-all", 6e4)).toEqual({ ok: true });
|
|
141
|
+
const second = await store.lock("run-all", 6e4);
|
|
142
|
+
expect(second.ok).toBe(false);
|
|
143
|
+
if (!second.ok) {
|
|
144
|
+
expect(second.retryAfterMs).toBeGreaterThan(0);
|
|
145
|
+
expect(second.retryAfterMs).toBeLessThanOrEqual(6e4);
|
|
146
|
+
}
|
|
147
|
+
expect(await store.lock("another", 6e4)).toEqual({ ok: true });
|
|
148
|
+
expect(await store.lock("short", 20)).toEqual({ ok: true });
|
|
149
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
150
|
+
expect(await store.lock("short", 20)).toEqual({ ok: true });
|
|
151
|
+
if (store.unlock) {
|
|
152
|
+
await store.unlock("run-all");
|
|
153
|
+
expect(await store.lock("run-all", 6e4)).toEqual({ ok: true });
|
|
154
|
+
await store.unlock("never-taken");
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
it("caches small values with a time-to-live (when supported)", async () => {
|
|
158
|
+
const store = await createStore();
|
|
159
|
+
if (!store.cache) return;
|
|
160
|
+
expect(await store.cache.get("missing")).toBeNull();
|
|
161
|
+
await store.cache.set("stats", '{"a":1}', 6e4);
|
|
162
|
+
expect(await store.cache.get("stats")).toBe('{"a":1}');
|
|
163
|
+
await store.cache.set("stats", '{"a":2}', 6e4);
|
|
164
|
+
expect(await store.cache.get("stats")).toBe('{"a":2}');
|
|
165
|
+
await store.cache.set("brief", "x", 20);
|
|
166
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
167
|
+
expect(await store.cache.get("brief")).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function mockFetch(routes) {
|
|
172
|
+
const calls = [];
|
|
173
|
+
const impl = async (input, init) => {
|
|
174
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
175
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
176
|
+
const headers = Object.fromEntries(new Headers(init?.headers).entries());
|
|
177
|
+
calls.push({ url, method, headers, body: typeof init?.body === "string" ? init.body : void 0 });
|
|
178
|
+
const route = routes.find(
|
|
179
|
+
(candidate) => (typeof candidate.url === "string" ? url.includes(candidate.url) : candidate.url.test(url)) && (!candidate.method || candidate.method.toUpperCase() === method)
|
|
180
|
+
);
|
|
181
|
+
if (!route) throw new TypeError(`mockFetch: no route for ${method} ${url}`);
|
|
182
|
+
if (route.delayMs) await new Promise((resolve) => setTimeout(resolve, route.delayMs));
|
|
183
|
+
if (route.error) throw route.error;
|
|
184
|
+
const body = route.body === void 0 ? "" : typeof route.body === "string" ? route.body : JSON.stringify(route.body);
|
|
185
|
+
return new Response(body || null, { status: route.status ?? 200, headers: route.headers });
|
|
186
|
+
};
|
|
187
|
+
return Object.assign(impl, { calls });
|
|
188
|
+
}
|
|
189
|
+
async function runCheck(service, options = {}) {
|
|
190
|
+
const [resolved] = [...chunkXHXEKB3Q_cjs.resolveTree([service]).services.values()];
|
|
191
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
192
|
+
return chunkPARE6EUZ_cjs.runService({
|
|
193
|
+
service: resolved,
|
|
194
|
+
secretValues: chunkPARE6EUZ_cjs.collectSecretValues([...Object.values(service.secrets), ...service.sensitive ?? []]),
|
|
195
|
+
fetch: options.fetch,
|
|
196
|
+
now: options.now,
|
|
197
|
+
hideResponseBodies: options.hideResponseBodies,
|
|
198
|
+
cache: {
|
|
199
|
+
get: async (key) => cache.get(key) ?? null,
|
|
200
|
+
set: async (key, value) => void cache.set(key, value)
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
exports.mockFetch = mockFetch;
|
|
206
|
+
exports.runCheck = runCheck;
|
|
207
|
+
exports.runStoreConformance = runStoreConformance;
|
|
208
|
+
//# sourceMappingURL=index.cjs.map
|
|
209
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/testing/index.ts"],"names":["resolveTree","runService","collectSecretValues"],"mappings":"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","file":"index.cjs","sourcesContent":["import { resolveTree, type ServiceDefinition } from \"../config\";\nimport { runService } from \"../runner\";\nimport { collectSecretValues } from \"../sanitize\";\nimport type { CheckSample, RunSummary, StatusStore } from \"../store\";\nimport type { CheckResult } from \"../types\";\n\nconst summary = (overrides: Partial<RunSummary> = {}): RunSummary => ({\n at: 1_000,\n trigger: \"scheduled\",\n full: true,\n score: 100,\n label: \"operational\",\n failing: {},\n ...overrides,\n});\n\nconst result = (overrides: Partial<CheckResult> = {}): CheckResult => ({\n key: \"api\",\n status: \"up\",\n checkedAt: 1_000,\n probed: true,\n latencyMs: 120,\n ...overrides,\n});\n\nconst sample = (overrides: Partial<CheckSample> = {}): CheckSample => ({\n key: \"api\",\n at: 1_000,\n status: \"up\",\n latencyMs: 120,\n ...overrides,\n});\n\n/** Stores may drop `undefined` fields or not: compare what matters */\nconst clean = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\n/** The few test-runner functions the suite needs. Vitest and Jest both fit. */\nexport type TestHarness = {\n describe: (name: string, fn: () => void) => void;\n it: (name: string, fn: () => Promise<void> | void) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n expect: (actual: any) => any;\n};\n\nfunction resolveHarness(harness?: TestHarness): TestHarness {\n if (harness) return harness;\n const globals = globalThis as Partial<TestHarness>;\n if (globals.describe && globals.it && globals.expect) {\n return { describe: globals.describe, it: globals.it, expect: globals.expect };\n }\n throw new Error(\n \"sermon-js/testing: pass your test runner's { describe, it, expect } as the third argument \" +\n \"(or enable the runner's globals).\"\n );\n}\n\n/**\n * The contract every `StatusStore` must honour. Call it from a test file of your adapter:\n *\n * ```ts\n * import { describe, expect, it } from \"vitest\"; // or Jest's globals\n * import { runStoreConformance } from \"sermon-js/testing\";\n * runStoreConformance(\"my store\", () => myStore({ … }), { describe, it, expect });\n * ```\n *\n * `createStore` must return a fresh, empty store each time.\n */\nexport function runStoreConformance(\n name: string,\n createStore: () => StatusStore | Promise<StatusStore>,\n harness?: TestHarness\n) {\n const { describe, it, expect } = resolveHarness(harness);\n describe(`${name}: StatusStore conformance`, () => {\n it(\"starts empty\", async () => {\n const store = await createStore();\n expect(await store.latest()).toEqual({});\n expect(await store.runs({ since: 0, until: 10_000 })).toEqual([]);\n expect(await store.samples(\"api\", { since: 0, until: 10_000 })).toEqual([]);\n });\n\n it(\"keeps the latest result of each service, with all its fields\", async () => {\n const store = await createStore();\n const rich = result({\n key: \"fly\",\n status: \"degraded\",\n httpStatus: 200,\n error: \"1/2 machine(s) down: sin\",\n metrics: { machines: \"1/2 running\", count: 2 },\n usage: [{ label: \"Requests\", used: 15, limit: 100, unit: \"requests\", resetsAt: 9_999 }],\n instances: [\n { key: \"ams\", region: \"ams\", id: \"m1\", state: \"started\", status: \"up\", latencyMs: 80, httpStatus: 200 },\n { key: \"sin\", region: \"sin\", id: \"m2\", state: \"stopped\", status: \"down\", error: \"Out of memory\" },\n ],\n });\n await store.saveRun({\n summary: summary(),\n results: [result(), rich, result({ key: \"off\", status: \"unconfigured\", probed: false, latencyMs: undefined })],\n samples: [sample(), sample({ key: \"fly\", status: \"degraded\" })],\n });\n await store.saveRun({\n summary: summary({ at: 2_000, full: false }),\n results: [result({ checkedAt: 2_000, status: \"down\", error: \"HTTP 500\", latencyMs: 300 })],\n samples: [sample({ at: 2_000, status: \"down\", error: \"HTTP 500\", latencyMs: 300 })],\n });\n\n const latest = await store.latest();\n expect(Object.keys(latest).sort()).toEqual([\"api\", \"fly\", \"off\"]);\n expect(clean(latest.api)).toEqual(clean(result({ checkedAt: 2_000, status: \"down\", error: \"HTTP 500\", latencyMs: 300 })));\n expect(clean(latest.fly)).toEqual(clean(rich));\n expect(latest.off?.status).toBe(\"unconfigured\");\n });\n\n it(\"returns runs within an inclusive range, oldest first\", async () => {\n const store = await createStore();\n for (const at of [3_000, 1_000, 2_000]) {\n await store.saveRun({\n summary: summary({ at, score: at / 100, failing: at === 2_000 ? { api: \"down\" } : {} }),\n results: [],\n samples: [],\n });\n }\n const runs = await store.runs({ since: 1_000, until: 2_000 });\n expect(runs.map((run) => run.at)).toEqual([1_000, 2_000]);\n expect(clean(runs[1])).toEqual(\n clean(summary({ at: 2_000, score: 20, failing: { api: \"down\" } }))\n );\n expect((await store.runs({ since: 0, until: 9_000 })).map((run) => run.at)).toEqual([1_000, 2_000, 3_000]);\n });\n\n it(\"returns samples of one service within an inclusive range, oldest first\", async () => {\n const store = await createStore();\n await store.saveRun({\n summary: summary({ at: 2_000 }),\n results: [],\n samples: [\n sample({ at: 2_000, instances: { ams: [\"up\", 80], sin: [\"sleeping\"] } }),\n sample({ key: \"other\", at: 2_000 }),\n ],\n });\n await store.saveRun({ summary: summary({ at: 1_000 }), results: [], samples: [sample({ at: 1_000 })] });\n await store.saveRun({ summary: summary({ at: 3_000 }), results: [], samples: [sample({ at: 3_000 })] });\n\n const samples = await store.samples(\"api\", { since: 1_000, until: 2_000 });\n expect(samples.map((s) => s.at)).toEqual([1_000, 2_000]);\n expect(samples.every((s) => s.key === \"api\")).toBe(true);\n expect(clean(samples[1]?.instances)).toEqual({ ams: [\"up\", 80], sin: [\"sleeping\"] });\n });\n\n it(\"prunes old runs and samples\", async () => {\n const store = await createStore();\n for (const at of [1_000, 2_000, 3_000]) {\n await store.saveRun({ summary: summary({ at }), results: [result({ checkedAt: at })], samples: [sample({ at })] });\n }\n await store.prune({ before: 2_000 });\n expect((await store.runs({ since: 0, until: 9_000 })).map((run) => run.at)).toEqual([2_000, 3_000]);\n expect((await store.samples(\"api\", { since: 0, until: 9_000 })).map((s) => s.at)).toEqual([2_000, 3_000]);\n // Latest results are not history: pruning by time keeps them\n expect(Object.keys(await store.latest())).toEqual([\"api\"]);\n });\n\n it(\"drops services that are not in keepKeys\", async () => {\n const store = await createStore();\n await store.saveRun({\n summary: summary(),\n results: [result(), result({ key: \"removed\" })],\n samples: [sample(), sample({ key: \"removed\" })],\n });\n await store.prune({ before: 0, keepKeys: [\"api\"] });\n expect(Object.keys(await store.latest())).toEqual([\"api\"]);\n expect(await store.samples(\"removed\", { since: 0, until: 9_000 })).toEqual([]);\n expect(await store.samples(\"api\", { since: 0, until: 9_000 })).toHaveLength(1);\n expect(await store.runs({ since: 0, until: 9_000 })).toHaveLength(1);\n });\n\n it(\"locks by name until the time-to-live passes (when supported)\", async () => {\n const store = await createStore();\n if (!store.lock) return;\n expect(await store.lock(\"run-all\", 60_000)).toEqual({ ok: true });\n const second = await store.lock(\"run-all\", 60_000);\n expect(second.ok).toBe(false);\n if (!second.ok) {\n expect(second.retryAfterMs).toBeGreaterThan(0);\n expect(second.retryAfterMs).toBeLessThanOrEqual(60_000);\n }\n expect(await store.lock(\"another\", 60_000)).toEqual({ ok: true });\n\n expect(await store.lock(\"short\", 20)).toEqual({ ok: true });\n await new Promise((resolve) => setTimeout(resolve, 60));\n expect(await store.lock(\"short\", 20)).toEqual({ ok: true });\n\n if (store.unlock) {\n await store.unlock(\"run-all\");\n expect(await store.lock(\"run-all\", 60_000)).toEqual({ ok: true });\n // Releasing a lock nobody holds is fine\n await store.unlock(\"never-taken\");\n }\n });\n\n it(\"caches small values with a time-to-live (when supported)\", async () => {\n const store = await createStore();\n if (!store.cache) return;\n expect(await store.cache.get(\"missing\")).toBeNull();\n await store.cache.set(\"stats\", '{\"a\":1}', 60_000);\n expect(await store.cache.get(\"stats\")).toBe('{\"a\":1}');\n await store.cache.set(\"stats\", '{\"a\":2}', 60_000);\n expect(await store.cache.get(\"stats\")).toBe('{\"a\":2}');\n\n await store.cache.set(\"brief\", \"x\", 20);\n await new Promise((resolve) => setTimeout(resolve, 60));\n expect(await store.cache.get(\"brief\")).toBeNull();\n });\n });\n}\n\nexport type MockRoute = {\n /** Matches when the request URL contains this string, or matches this pattern */\n url: string | RegExp;\n method?: string;\n status?: number;\n /** JSON-serialized when it's an object */\n body?: unknown;\n headers?: Record<string, string>;\n /** Reject the request like a network failure */\n error?: Error;\n /** Delay the response, to simulate latency */\n delayMs?: number;\n};\n\nexport type MockFetch = typeof fetch & {\n /** Every request made, in order */\n calls: Array<{ url: string; method: string; headers: Record<string, string>; body?: string }>;\n};\n\n/**\n * A `fetch` that answers from a list of routes (first match wins) and records every call.\n * Unmatched requests fail, so tests prove that nothing unexpected is called.\n */\nexport function mockFetch(routes: MockRoute[]): MockFetch {\n const calls: MockFetch[\"calls\"] = [];\n const impl = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.href : input.url;\n const method = (init?.method ?? \"GET\").toUpperCase();\n const headers = Object.fromEntries(new Headers(init?.headers).entries());\n calls.push({ url, method, headers, body: typeof init?.body === \"string\" ? init.body : undefined });\n\n const route = routes.find(\n (candidate) =>\n (typeof candidate.url === \"string\" ? url.includes(candidate.url) : candidate.url.test(url)) &&\n (!candidate.method || candidate.method.toUpperCase() === method)\n );\n if (!route) throw new TypeError(`mockFetch: no route for ${method} ${url}`);\n if (route.delayMs) await new Promise((resolve) => setTimeout(resolve, route.delayMs));\n if (route.error) throw route.error;\n const body =\n route.body === undefined ? \"\" : typeof route.body === \"string\" ? route.body : JSON.stringify(route.body);\n // Null-body statuses (204, 304…) reject a body, even an empty one\n return new Response(body || null, { status: route.status ?? 200, headers: route.headers });\n };\n return Object.assign(impl as typeof fetch, { calls });\n}\n\n/**\n * Run one service exactly as the engine would (shared rules, redaction, timing) and return\n * its result. For testing presets and custom checks:\n *\n * ```ts\n * const fetch = mockFetch([{ url: \"/health\", status: 200 }]);\n * const result = await runCheck(myPreset({ … }), { fetch });\n * ```\n */\nexport async function runCheck(\n service: ServiceDefinition,\n options: { fetch?: typeof fetch; now?: () => number; cache?: Map<string, string>; hideResponseBodies?: boolean } = {}\n): Promise<CheckResult> {\n const [resolved] = [...resolveTree([service]).services.values()];\n const cache = options.cache ?? new Map<string, string>();\n return runService({\n service: resolved!,\n secretValues: collectSecretValues([...Object.values(service.secrets), ...(service.sensitive ?? [])]),\n fetch: options.fetch,\n now: options.now,\n hideResponseBodies: options.hideResponseBodies,\n cache: {\n get: async (key) => cache.get(key) ?? null,\n set: async (key, value) => void cache.set(key, value),\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { S as ServiceDefinition } from '../config-BEbP-uLf.cjs';
|
|
2
|
+
import { S as StatusStore } from '../store-DybhDphS.cjs';
|
|
3
|
+
import { g as CheckResult } from '../types-CctvUAs2.cjs';
|
|
4
|
+
|
|
5
|
+
/** The few test-runner functions the suite needs. Vitest and Jest both fit. */
|
|
6
|
+
type TestHarness = {
|
|
7
|
+
describe: (name: string, fn: () => void) => void;
|
|
8
|
+
it: (name: string, fn: () => Promise<void> | void) => void;
|
|
9
|
+
expect: (actual: any) => any;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The contract every `StatusStore` must honour. Call it from a test file of your adapter:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { describe, expect, it } from "vitest"; // or Jest's globals
|
|
16
|
+
* import { runStoreConformance } from "sermon-js/testing";
|
|
17
|
+
* runStoreConformance("my store", () => myStore({ … }), { describe, it, expect });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `createStore` must return a fresh, empty store each time.
|
|
21
|
+
*/
|
|
22
|
+
declare function runStoreConformance(name: string, createStore: () => StatusStore | Promise<StatusStore>, harness?: TestHarness): void;
|
|
23
|
+
type MockRoute = {
|
|
24
|
+
/** Matches when the request URL contains this string, or matches this pattern */
|
|
25
|
+
url: string | RegExp;
|
|
26
|
+
method?: string;
|
|
27
|
+
status?: number;
|
|
28
|
+
/** JSON-serialized when it's an object */
|
|
29
|
+
body?: unknown;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/** Reject the request like a network failure */
|
|
32
|
+
error?: Error;
|
|
33
|
+
/** Delay the response, to simulate latency */
|
|
34
|
+
delayMs?: number;
|
|
35
|
+
};
|
|
36
|
+
type MockFetch = typeof fetch & {
|
|
37
|
+
/** Every request made, in order */
|
|
38
|
+
calls: Array<{
|
|
39
|
+
url: string;
|
|
40
|
+
method: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
body?: string;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* A `fetch` that answers from a list of routes (first match wins) and records every call.
|
|
47
|
+
* Unmatched requests fail, so tests prove that nothing unexpected is called.
|
|
48
|
+
*/
|
|
49
|
+
declare function mockFetch(routes: MockRoute[]): MockFetch;
|
|
50
|
+
/**
|
|
51
|
+
* Run one service exactly as the engine would (shared rules, redaction, timing) and return
|
|
52
|
+
* its result. For testing presets and custom checks:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const fetch = mockFetch([{ url: "/health", status: 200 }]);
|
|
56
|
+
* const result = await runCheck(myPreset({ … }), { fetch });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function runCheck(service: ServiceDefinition, options?: {
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
cache?: Map<string, string>;
|
|
63
|
+
hideResponseBodies?: boolean;
|
|
64
|
+
}): Promise<CheckResult>;
|
|
65
|
+
|
|
66
|
+
export { type MockFetch, type MockRoute, type TestHarness, mockFetch, runCheck, runStoreConformance };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { S as ServiceDefinition } from '../config-w1tm1bdu.js';
|
|
2
|
+
import { S as StatusStore } from '../store-_GuzSP0z.js';
|
|
3
|
+
import { g as CheckResult } from '../types-CctvUAs2.js';
|
|
4
|
+
|
|
5
|
+
/** The few test-runner functions the suite needs. Vitest and Jest both fit. */
|
|
6
|
+
type TestHarness = {
|
|
7
|
+
describe: (name: string, fn: () => void) => void;
|
|
8
|
+
it: (name: string, fn: () => Promise<void> | void) => void;
|
|
9
|
+
expect: (actual: any) => any;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The contract every `StatusStore` must honour. Call it from a test file of your adapter:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { describe, expect, it } from "vitest"; // or Jest's globals
|
|
16
|
+
* import { runStoreConformance } from "sermon-js/testing";
|
|
17
|
+
* runStoreConformance("my store", () => myStore({ … }), { describe, it, expect });
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `createStore` must return a fresh, empty store each time.
|
|
21
|
+
*/
|
|
22
|
+
declare function runStoreConformance(name: string, createStore: () => StatusStore | Promise<StatusStore>, harness?: TestHarness): void;
|
|
23
|
+
type MockRoute = {
|
|
24
|
+
/** Matches when the request URL contains this string, or matches this pattern */
|
|
25
|
+
url: string | RegExp;
|
|
26
|
+
method?: string;
|
|
27
|
+
status?: number;
|
|
28
|
+
/** JSON-serialized when it's an object */
|
|
29
|
+
body?: unknown;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
/** Reject the request like a network failure */
|
|
32
|
+
error?: Error;
|
|
33
|
+
/** Delay the response, to simulate latency */
|
|
34
|
+
delayMs?: number;
|
|
35
|
+
};
|
|
36
|
+
type MockFetch = typeof fetch & {
|
|
37
|
+
/** Every request made, in order */
|
|
38
|
+
calls: Array<{
|
|
39
|
+
url: string;
|
|
40
|
+
method: string;
|
|
41
|
+
headers: Record<string, string>;
|
|
42
|
+
body?: string;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* A `fetch` that answers from a list of routes (first match wins) and records every call.
|
|
47
|
+
* Unmatched requests fail, so tests prove that nothing unexpected is called.
|
|
48
|
+
*/
|
|
49
|
+
declare function mockFetch(routes: MockRoute[]): MockFetch;
|
|
50
|
+
/**
|
|
51
|
+
* Run one service exactly as the engine would (shared rules, redaction, timing) and return
|
|
52
|
+
* its result. For testing presets and custom checks:
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const fetch = mockFetch([{ url: "/health", status: 200 }]);
|
|
56
|
+
* const result = await runCheck(myPreset({ … }), { fetch });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
declare function runCheck(service: ServiceDefinition, options?: {
|
|
60
|
+
fetch?: typeof fetch;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
cache?: Map<string, string>;
|
|
63
|
+
hideResponseBodies?: boolean;
|
|
64
|
+
}): Promise<CheckResult>;
|
|
65
|
+
|
|
66
|
+
export { type MockFetch, type MockRoute, type TestHarness, mockFetch, runCheck, runStoreConformance };
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { runService, collectSecretValues } from '../chunk-2M4UBCDM.js';
|
|
2
|
+
import { resolveTree } from '../chunk-XPXIDJUU.js';
|
|
3
|
+
|
|
4
|
+
// src/testing/index.ts
|
|
5
|
+
var summary = (overrides = {}) => ({
|
|
6
|
+
at: 1e3,
|
|
7
|
+
trigger: "scheduled",
|
|
8
|
+
full: true,
|
|
9
|
+
score: 100,
|
|
10
|
+
label: "operational",
|
|
11
|
+
failing: {},
|
|
12
|
+
...overrides
|
|
13
|
+
});
|
|
14
|
+
var result = (overrides = {}) => ({
|
|
15
|
+
key: "api",
|
|
16
|
+
status: "up",
|
|
17
|
+
checkedAt: 1e3,
|
|
18
|
+
probed: true,
|
|
19
|
+
latencyMs: 120,
|
|
20
|
+
...overrides
|
|
21
|
+
});
|
|
22
|
+
var sample = (overrides = {}) => ({
|
|
23
|
+
key: "api",
|
|
24
|
+
at: 1e3,
|
|
25
|
+
status: "up",
|
|
26
|
+
latencyMs: 120,
|
|
27
|
+
...overrides
|
|
28
|
+
});
|
|
29
|
+
var clean = (value) => JSON.parse(JSON.stringify(value));
|
|
30
|
+
function resolveHarness(harness) {
|
|
31
|
+
if (harness) return harness;
|
|
32
|
+
const globals = globalThis;
|
|
33
|
+
if (globals.describe && globals.it && globals.expect) {
|
|
34
|
+
return { describe: globals.describe, it: globals.it, expect: globals.expect };
|
|
35
|
+
}
|
|
36
|
+
throw new Error(
|
|
37
|
+
"sermon-js/testing: pass your test runner's { describe, it, expect } as the third argument (or enable the runner's globals)."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
function runStoreConformance(name, createStore, harness) {
|
|
41
|
+
const { describe, it, expect } = resolveHarness(harness);
|
|
42
|
+
describe(`${name}: StatusStore conformance`, () => {
|
|
43
|
+
it("starts empty", async () => {
|
|
44
|
+
const store = await createStore();
|
|
45
|
+
expect(await store.latest()).toEqual({});
|
|
46
|
+
expect(await store.runs({ since: 0, until: 1e4 })).toEqual([]);
|
|
47
|
+
expect(await store.samples("api", { since: 0, until: 1e4 })).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
it("keeps the latest result of each service, with all its fields", async () => {
|
|
50
|
+
const store = await createStore();
|
|
51
|
+
const rich = result({
|
|
52
|
+
key: "fly",
|
|
53
|
+
status: "degraded",
|
|
54
|
+
httpStatus: 200,
|
|
55
|
+
error: "1/2 machine(s) down: sin",
|
|
56
|
+
metrics: { machines: "1/2 running", count: 2 },
|
|
57
|
+
usage: [{ label: "Requests", used: 15, limit: 100, unit: "requests", resetsAt: 9999 }],
|
|
58
|
+
instances: [
|
|
59
|
+
{ key: "ams", region: "ams", id: "m1", state: "started", status: "up", latencyMs: 80, httpStatus: 200 },
|
|
60
|
+
{ key: "sin", region: "sin", id: "m2", state: "stopped", status: "down", error: "Out of memory" }
|
|
61
|
+
]
|
|
62
|
+
});
|
|
63
|
+
await store.saveRun({
|
|
64
|
+
summary: summary(),
|
|
65
|
+
results: [result(), rich, result({ key: "off", status: "unconfigured", probed: false, latencyMs: void 0 })],
|
|
66
|
+
samples: [sample(), sample({ key: "fly", status: "degraded" })]
|
|
67
|
+
});
|
|
68
|
+
await store.saveRun({
|
|
69
|
+
summary: summary({ at: 2e3, full: false }),
|
|
70
|
+
results: [result({ checkedAt: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })],
|
|
71
|
+
samples: [sample({ at: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })]
|
|
72
|
+
});
|
|
73
|
+
const latest = await store.latest();
|
|
74
|
+
expect(Object.keys(latest).sort()).toEqual(["api", "fly", "off"]);
|
|
75
|
+
expect(clean(latest.api)).toEqual(clean(result({ checkedAt: 2e3, status: "down", error: "HTTP 500", latencyMs: 300 })));
|
|
76
|
+
expect(clean(latest.fly)).toEqual(clean(rich));
|
|
77
|
+
expect(latest.off?.status).toBe("unconfigured");
|
|
78
|
+
});
|
|
79
|
+
it("returns runs within an inclusive range, oldest first", async () => {
|
|
80
|
+
const store = await createStore();
|
|
81
|
+
for (const at of [3e3, 1e3, 2e3]) {
|
|
82
|
+
await store.saveRun({
|
|
83
|
+
summary: summary({ at, score: at / 100, failing: at === 2e3 ? { api: "down" } : {} }),
|
|
84
|
+
results: [],
|
|
85
|
+
samples: []
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const runs = await store.runs({ since: 1e3, until: 2e3 });
|
|
89
|
+
expect(runs.map((run) => run.at)).toEqual([1e3, 2e3]);
|
|
90
|
+
expect(clean(runs[1])).toEqual(
|
|
91
|
+
clean(summary({ at: 2e3, score: 20, failing: { api: "down" } }))
|
|
92
|
+
);
|
|
93
|
+
expect((await store.runs({ since: 0, until: 9e3 })).map((run) => run.at)).toEqual([1e3, 2e3, 3e3]);
|
|
94
|
+
});
|
|
95
|
+
it("returns samples of one service within an inclusive range, oldest first", async () => {
|
|
96
|
+
const store = await createStore();
|
|
97
|
+
await store.saveRun({
|
|
98
|
+
summary: summary({ at: 2e3 }),
|
|
99
|
+
results: [],
|
|
100
|
+
samples: [
|
|
101
|
+
sample({ at: 2e3, instances: { ams: ["up", 80], sin: ["sleeping"] } }),
|
|
102
|
+
sample({ key: "other", at: 2e3 })
|
|
103
|
+
]
|
|
104
|
+
});
|
|
105
|
+
await store.saveRun({ summary: summary({ at: 1e3 }), results: [], samples: [sample({ at: 1e3 })] });
|
|
106
|
+
await store.saveRun({ summary: summary({ at: 3e3 }), results: [], samples: [sample({ at: 3e3 })] });
|
|
107
|
+
const samples = await store.samples("api", { since: 1e3, until: 2e3 });
|
|
108
|
+
expect(samples.map((s) => s.at)).toEqual([1e3, 2e3]);
|
|
109
|
+
expect(samples.every((s) => s.key === "api")).toBe(true);
|
|
110
|
+
expect(clean(samples[1]?.instances)).toEqual({ ams: ["up", 80], sin: ["sleeping"] });
|
|
111
|
+
});
|
|
112
|
+
it("prunes old runs and samples", async () => {
|
|
113
|
+
const store = await createStore();
|
|
114
|
+
for (const at of [1e3, 2e3, 3e3]) {
|
|
115
|
+
await store.saveRun({ summary: summary({ at }), results: [result({ checkedAt: at })], samples: [sample({ at })] });
|
|
116
|
+
}
|
|
117
|
+
await store.prune({ before: 2e3 });
|
|
118
|
+
expect((await store.runs({ since: 0, until: 9e3 })).map((run) => run.at)).toEqual([2e3, 3e3]);
|
|
119
|
+
expect((await store.samples("api", { since: 0, until: 9e3 })).map((s) => s.at)).toEqual([2e3, 3e3]);
|
|
120
|
+
expect(Object.keys(await store.latest())).toEqual(["api"]);
|
|
121
|
+
});
|
|
122
|
+
it("drops services that are not in keepKeys", async () => {
|
|
123
|
+
const store = await createStore();
|
|
124
|
+
await store.saveRun({
|
|
125
|
+
summary: summary(),
|
|
126
|
+
results: [result(), result({ key: "removed" })],
|
|
127
|
+
samples: [sample(), sample({ key: "removed" })]
|
|
128
|
+
});
|
|
129
|
+
await store.prune({ before: 0, keepKeys: ["api"] });
|
|
130
|
+
expect(Object.keys(await store.latest())).toEqual(["api"]);
|
|
131
|
+
expect(await store.samples("removed", { since: 0, until: 9e3 })).toEqual([]);
|
|
132
|
+
expect(await store.samples("api", { since: 0, until: 9e3 })).toHaveLength(1);
|
|
133
|
+
expect(await store.runs({ since: 0, until: 9e3 })).toHaveLength(1);
|
|
134
|
+
});
|
|
135
|
+
it("locks by name until the time-to-live passes (when supported)", async () => {
|
|
136
|
+
const store = await createStore();
|
|
137
|
+
if (!store.lock) return;
|
|
138
|
+
expect(await store.lock("run-all", 6e4)).toEqual({ ok: true });
|
|
139
|
+
const second = await store.lock("run-all", 6e4);
|
|
140
|
+
expect(second.ok).toBe(false);
|
|
141
|
+
if (!second.ok) {
|
|
142
|
+
expect(second.retryAfterMs).toBeGreaterThan(0);
|
|
143
|
+
expect(second.retryAfterMs).toBeLessThanOrEqual(6e4);
|
|
144
|
+
}
|
|
145
|
+
expect(await store.lock("another", 6e4)).toEqual({ ok: true });
|
|
146
|
+
expect(await store.lock("short", 20)).toEqual({ ok: true });
|
|
147
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
148
|
+
expect(await store.lock("short", 20)).toEqual({ ok: true });
|
|
149
|
+
if (store.unlock) {
|
|
150
|
+
await store.unlock("run-all");
|
|
151
|
+
expect(await store.lock("run-all", 6e4)).toEqual({ ok: true });
|
|
152
|
+
await store.unlock("never-taken");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
it("caches small values with a time-to-live (when supported)", async () => {
|
|
156
|
+
const store = await createStore();
|
|
157
|
+
if (!store.cache) return;
|
|
158
|
+
expect(await store.cache.get("missing")).toBeNull();
|
|
159
|
+
await store.cache.set("stats", '{"a":1}', 6e4);
|
|
160
|
+
expect(await store.cache.get("stats")).toBe('{"a":1}');
|
|
161
|
+
await store.cache.set("stats", '{"a":2}', 6e4);
|
|
162
|
+
expect(await store.cache.get("stats")).toBe('{"a":2}');
|
|
163
|
+
await store.cache.set("brief", "x", 20);
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
165
|
+
expect(await store.cache.get("brief")).toBeNull();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function mockFetch(routes) {
|
|
170
|
+
const calls = [];
|
|
171
|
+
const impl = async (input, init) => {
|
|
172
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
173
|
+
const method = (init?.method ?? "GET").toUpperCase();
|
|
174
|
+
const headers = Object.fromEntries(new Headers(init?.headers).entries());
|
|
175
|
+
calls.push({ url, method, headers, body: typeof init?.body === "string" ? init.body : void 0 });
|
|
176
|
+
const route = routes.find(
|
|
177
|
+
(candidate) => (typeof candidate.url === "string" ? url.includes(candidate.url) : candidate.url.test(url)) && (!candidate.method || candidate.method.toUpperCase() === method)
|
|
178
|
+
);
|
|
179
|
+
if (!route) throw new TypeError(`mockFetch: no route for ${method} ${url}`);
|
|
180
|
+
if (route.delayMs) await new Promise((resolve) => setTimeout(resolve, route.delayMs));
|
|
181
|
+
if (route.error) throw route.error;
|
|
182
|
+
const body = route.body === void 0 ? "" : typeof route.body === "string" ? route.body : JSON.stringify(route.body);
|
|
183
|
+
return new Response(body || null, { status: route.status ?? 200, headers: route.headers });
|
|
184
|
+
};
|
|
185
|
+
return Object.assign(impl, { calls });
|
|
186
|
+
}
|
|
187
|
+
async function runCheck(service, options = {}) {
|
|
188
|
+
const [resolved] = [...resolveTree([service]).services.values()];
|
|
189
|
+
const cache = options.cache ?? /* @__PURE__ */ new Map();
|
|
190
|
+
return runService({
|
|
191
|
+
service: resolved,
|
|
192
|
+
secretValues: collectSecretValues([...Object.values(service.secrets), ...service.sensitive ?? []]),
|
|
193
|
+
fetch: options.fetch,
|
|
194
|
+
now: options.now,
|
|
195
|
+
hideResponseBodies: options.hideResponseBodies,
|
|
196
|
+
cache: {
|
|
197
|
+
get: async (key) => cache.get(key) ?? null,
|
|
198
|
+
set: async (key, value) => void cache.set(key, value)
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export { mockFetch, runCheck, runStoreConformance };
|
|
204
|
+
//# sourceMappingURL=index.js.map
|
|
205
|
+
//# sourceMappingURL=index.js.map
|