mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
refactor(core): message tracker abstraction and formatter plugin registry
This commit is contained in:
parent
486f38365f
commit
eaec039ad4
9 changed files with 361 additions and 90 deletions
89
tests/formatter-registry.test.ts
Normal file
89
tests/formatter-registry.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { describe, it, expect } from "bun:test";
|
||||
import { findFormatter } from "../server/lib/formatters/registry";
|
||||
import { formatEvent } from "../server/lib/formatters";
|
||||
import type { Route, WebhookEvent } from "../server/lib/types";
|
||||
|
||||
const SUPPORTED_EVENTS = [
|
||||
"push",
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"pull_request_review_comment",
|
||||
"issues",
|
||||
"issue_comment",
|
||||
"workflow_run",
|
||||
"workflow_job",
|
||||
"status",
|
||||
"deployment",
|
||||
"ping",
|
||||
"release",
|
||||
"create",
|
||||
"delete",
|
||||
"star",
|
||||
"fork",
|
||||
"check_run",
|
||||
"check_suite",
|
||||
"commit_comment",
|
||||
"deployment_status",
|
||||
"member",
|
||||
"label",
|
||||
"milestone",
|
||||
"discussion",
|
||||
"discussion_comment",
|
||||
"repository",
|
||||
"code_scanning_alert",
|
||||
"dependabot_alert",
|
||||
"custom",
|
||||
];
|
||||
|
||||
function makeRoute(): Route {
|
||||
return {
|
||||
id: "r1",
|
||||
name: "test",
|
||||
enabled: true,
|
||||
filters: [],
|
||||
targets: [{ platform: "discord", channelId: "c1" }],
|
||||
};
|
||||
}
|
||||
|
||||
describe("findFormatter", () => {
|
||||
it("maps every supported event type to a formatter", () => {
|
||||
for (const event of SUPPORTED_EVENTS) {
|
||||
const formatter = findFormatter(event);
|
||||
expect(formatter, `missing formatter for ${event}`).toBeDefined();
|
||||
expect(formatter!.events).toContain(event);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown event type", () => {
|
||||
expect(findFormatter("totally_unknown_event")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatEvent", () => {
|
||||
it("falls back to the generic formatter for an unknown event", () => {
|
||||
const route = makeRoute();
|
||||
const event: WebhookEvent = {
|
||||
event: "totally_unknown_event",
|
||||
payload: { action: "opened" },
|
||||
};
|
||||
const message = formatEvent(route, event);
|
||||
expect(message).toBeDefined();
|
||||
expect(typeof message.title).toBe("string");
|
||||
expect(message.color).toBe(9147550);
|
||||
});
|
||||
|
||||
it("routes a known event through its dedicated formatter", () => {
|
||||
const route = makeRoute();
|
||||
const event: WebhookEvent = {
|
||||
event: "issues",
|
||||
payload: {
|
||||
action: "opened",
|
||||
repository: { full_name: "acme/widget" },
|
||||
issue: { number: 7, title: "Add feature" },
|
||||
sender: { login: "alice" },
|
||||
},
|
||||
};
|
||||
const message = formatEvent(route, event);
|
||||
expect(message.title).toContain("acme/widget#7");
|
||||
});
|
||||
});
|
||||
59
tests/message-tracker.test.ts
Normal file
59
tests/message-tracker.test.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { describe, it, expect } from "bun:test";
|
||||
import { kvMessageTracker } from "../server/lib/lib/message-tracker";
|
||||
|
||||
function createMockKV(): KVNamespace {
|
||||
const store = new Map<string, string>();
|
||||
const ttl = new Map<string, number>();
|
||||
return {
|
||||
get: async (key: string) => (store.has(key) ? store.get(key)! : null),
|
||||
put: async (key: string, value: string, opts?: { expirationTtl?: number }) => {
|
||||
store.set(key, value);
|
||||
if (opts?.expirationTtl) ttl.set(key, opts.expirationTtl);
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
store.delete(key);
|
||||
},
|
||||
list: async () => ({ keys: [], list_complete: true, cacheStatus: null }),
|
||||
} as unknown as KVNamespace;
|
||||
}
|
||||
|
||||
describe("kvMessageTracker", () => {
|
||||
it("stores and retrieves a message id by event and target", async () => {
|
||||
const kv = createMockKV();
|
||||
const tracker = kvMessageTracker(kv);
|
||||
await tracker.set("route-1:workflow-9", "channel-1", "msg-42");
|
||||
expect(await tracker.get("route-1:workflow-9", "channel-1")).toBe("msg-42");
|
||||
});
|
||||
|
||||
it("returns null for an unknown event/target", async () => {
|
||||
const kv = createMockKV();
|
||||
const tracker = kvMessageTracker(kv);
|
||||
expect(await tracker.get("missing", "channel-1")).toBeNull();
|
||||
});
|
||||
|
||||
it("uses the msg:{eventId}:{targetId} key with a 7-day TTL", async () => {
|
||||
const kv = createMockKV();
|
||||
const keys = new Map<string, string>();
|
||||
const ttls = new Map<string, number>();
|
||||
(kv.put as unknown) = async (
|
||||
key: string,
|
||||
value: string,
|
||||
opts?: { expirationTtl?: number },
|
||||
): Promise<void> => {
|
||||
keys.set(key, value);
|
||||
ttls.set(key, opts?.expirationTtl ?? 0);
|
||||
};
|
||||
const tracker = kvMessageTracker(kv);
|
||||
await tracker.set("event-1", "target-1", "msg-7");
|
||||
expect(keys.has("msg:event-1:target-1")).toBe(true);
|
||||
expect(ttls.get("msg:event-1:target-1")).toBe(604800);
|
||||
});
|
||||
|
||||
it("deletes tracked entries", async () => {
|
||||
const kv = createMockKV();
|
||||
const tracker = kvMessageTracker(kv);
|
||||
await tracker.set("event-1", "target-1", "msg-7");
|
||||
await tracker.delete("event-1", "target-1");
|
||||
expect(await tracker.get("event-1", "target-1")).toBeNull();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue