mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { FilterNode, WebhookEvent } from "../server/lib/types";
|
|
import {
|
|
evaluateFilterNode,
|
|
containsKeyword,
|
|
explainFilter,
|
|
explainFilterNode,
|
|
} from "../server/lib/events/filter-ast";
|
|
import { matchRoute } from "../server/lib/events/match";
|
|
|
|
function event(overrides: Partial<WebhookEvent> = {}): WebhookEvent {
|
|
return {
|
|
event: "pull_request",
|
|
payload: {
|
|
repository: { full_name: "acme/widget" },
|
|
sender: { login: "alice" },
|
|
action: "opened",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("filter-ast evaluateFilterNode", () => {
|
|
test("leaf event matches", () => {
|
|
expect(evaluateFilterNode({ type: "event", match: "pull_request" }, event())).toBe(true);
|
|
});
|
|
|
|
test("leaf repo mismatch", () => {
|
|
expect(evaluateFilterNode({ type: "repo", match: "other/repo" }, event())).toBe(false);
|
|
});
|
|
|
|
test("all requires every child", () => {
|
|
const node: FilterNode = {
|
|
all: [
|
|
{ type: "event", match: "pull_request" },
|
|
{ type: "repo", match: "acme/widget" },
|
|
],
|
|
};
|
|
expect(evaluateFilterNode(node, event())).toBe(true);
|
|
});
|
|
|
|
test("all fails when one child fails", () => {
|
|
const node: FilterNode = {
|
|
all: [
|
|
{ type: "event", match: "pull_request" },
|
|
{ type: "repo", match: "nope/repo" },
|
|
],
|
|
};
|
|
expect(evaluateFilterNode(node, event())).toBe(false);
|
|
});
|
|
|
|
test("any passes when one child matches", () => {
|
|
const node: FilterNode = {
|
|
any: [
|
|
{ type: "repo", match: "nope/repo" },
|
|
{ type: "event", match: "pull_request" },
|
|
],
|
|
};
|
|
expect(evaluateFilterNode(node, event())).toBe(true);
|
|
});
|
|
|
|
test("not negates", () => {
|
|
expect(evaluateFilterNode({ not: { type: "repo", match: "nope/repo" } }, event())).toBe(true);
|
|
});
|
|
|
|
test("nested structure", () => {
|
|
const node: FilterNode = {
|
|
all: [
|
|
{ type: "event", match: "pull_request" },
|
|
{
|
|
any: [
|
|
{ type: "repo", match: "acme/*" },
|
|
{ type: "repo", match: "x/*" },
|
|
],
|
|
},
|
|
{ not: { type: "actor", match: "bob" } },
|
|
],
|
|
};
|
|
expect(evaluateFilterNode(node, event())).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("filter-ast containsKeyword", () => {
|
|
test("detects keyword leaf", () => {
|
|
expect(containsKeyword({ type: "keyword", match: "TODO" })).toBe(true);
|
|
});
|
|
|
|
test("detects nested keyword", () => {
|
|
expect(
|
|
containsKeyword({
|
|
all: [{ type: "event", match: "push" }, { not: { type: "keyword", match: "TODO" } }],
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("false without keyword", () => {
|
|
expect(containsKeyword({ all: [{ type: "event", match: "push" }] })).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("filter-ast explain", () => {
|
|
test("explainFilter leaf", () => {
|
|
expect(explainFilter({ type: "event", match: "push" })).toBe('event is "push"');
|
|
});
|
|
|
|
test("explainFilter array match", () => {
|
|
expect(explainFilter({ type: "repo", match: ["a/*", "b/*"] })).toBe('repo is "a/*" or "b/*"');
|
|
});
|
|
|
|
test("explainFilter exclude", () => {
|
|
expect(explainFilter({ type: "actor", match: "bob", exclude: true })).toBe(
|
|
'not (actor is "bob")',
|
|
);
|
|
});
|
|
|
|
test("explainFilterNode all", () => {
|
|
expect(
|
|
explainFilterNode({
|
|
all: [
|
|
{ type: "event", match: "push" },
|
|
{ type: "repo", match: "acme/*" },
|
|
],
|
|
}),
|
|
).toBe('(event is "push" and repo is "acme/*")');
|
|
});
|
|
|
|
test("explainFilterNode not", () => {
|
|
expect(explainFilterNode({ not: { type: "event", match: "push" } })).toBe(
|
|
'not (event is "push")',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("matchRoute AST integration", () => {
|
|
const baseRoute = {
|
|
id: "r1",
|
|
name: "route",
|
|
enabled: true,
|
|
filters: [{ type: "event" as const, match: "pull_request" }],
|
|
targets: [{ platform: "discord" as const, channelId: "c1" }],
|
|
};
|
|
|
|
test("flat filters act as AND", () => {
|
|
const route = {
|
|
...baseRoute,
|
|
filters: [
|
|
{ type: "event" as const, match: "pull_request" },
|
|
{ type: "repo" as const, match: "acme/widget" },
|
|
],
|
|
};
|
|
expect(matchRoute(route, event())).toBe(true);
|
|
});
|
|
|
|
test("ast overrides flat filters", () => {
|
|
const route = {
|
|
...baseRoute,
|
|
ast: {
|
|
any: [
|
|
{ type: "repo" as const, match: "nope/*" },
|
|
{ type: "event" as const, match: "pull_request" },
|
|
],
|
|
} as FilterNode,
|
|
};
|
|
expect(matchRoute(route, event())).toBe(true);
|
|
});
|
|
|
|
test("disabled route never matches", () => {
|
|
expect(matchRoute({ ...baseRoute, enabled: false }, event())).toBe(false);
|
|
});
|
|
});
|