mirror of
https://github.com/ReCloudStudio/WebHooker.git
synced 2026-09-22 16:11:29 +00:00
chore(quality): provider fixtures, formatter snapshots, platform contract tests, CI, CodeQL, dependabot
This commit is contained in:
parent
04fa52db52
commit
320e825bf8
15 changed files with 405 additions and 3 deletions
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
18
.github/workflows/ci.yml
vendored
Normal file
18
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun test
|
||||
- run: bun run lint
|
||||
26
.github/workflows/codeql.yml
vendored
Normal file
26
.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0"
|
||||
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
|
@ -122,6 +122,8 @@ server/ # Nitro server
|
|||
└── locales/ # en.ts, zh.ts translation dictionaries
|
||||
|
||||
tests/ # bun test unit tests (webhook, formatter, discord, telegram, admin, groups, invites, audit, send-log, token-store, ...)
|
||||
tests/fixtures/ # provider payload fixtures (github/gitea/custom) feeding provider + formatter tests
|
||||
tests/__snapshots__/ # formatter snapshot golden files (toMatchSnapshot)
|
||||
```
|
||||
|
||||
## Responsibilities
|
||||
|
|
@ -200,6 +202,13 @@ bun run lint # ESLint
|
|||
bun test # Unit tests (under tests/)
|
||||
```
|
||||
|
||||
Test suites beyond the per-module unit tests: provider fixtures (`tests/fixtures/` +
|
||||
`tests/provider-fixtures.test.ts`), formatter snapshots (`tests/formatter-snapshot.test.ts` →
|
||||
`tests/__snapshots__/`), and platform contract tests (`tests/platform-contract.test.ts`, which
|
||||
assert the Discord/Telegram renderers clamp to their platform limits). CI
|
||||
(`.github/workflows/ci.yml`) runs `bun install --frozen-lockfile` + `bun test` + `bun run lint`;
|
||||
CodeQL (`codeql.yml`) and Dependabot (`dependabot.yml`) are configured under `.github/`.
|
||||
|
||||
## Documentation
|
||||
|
||||
Keep every functional change in sync with the docs. After implementing a feature, fix,
|
||||
|
|
|
|||
|
|
@ -75,8 +75,14 @@ server/ # Nitro server (H3 handlers in server/routes/)
|
|||
└── locales/ # en.ts, zh.ts translation dictionaries
|
||||
|
||||
tests/ # Unit tests (bun test)
|
||||
tests/fixtures/ # Provider payload fixtures (github/gitea/custom)
|
||||
tests/__snapshots__/ # Formatter snapshot golden files
|
||||
```
|
||||
|
||||
Continuous integration (`.github/workflows/ci.yml`) runs `bun install --frozen-lockfile`,
|
||||
`bun test`, and `bun run lint`. CodeQL (`codeql.yml`) and Dependabot (`dependabot.yml`) are
|
||||
configured under `.github/`.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | Description |
|
||||
|
|
@ -112,6 +118,12 @@ bun test
|
|||
curl http://localhost:8787/health
|
||||
```
|
||||
|
||||
Beyond the per-module unit tests, the suite includes provider fixtures
|
||||
(`tests/fixtures/` + `tests/provider-fixtures.test.ts`), formatter snapshots
|
||||
(`tests/formatter-snapshot.test.ts`), and platform contract tests
|
||||
(`tests/platform-contract.test.ts`) that assert the Discord/Telegram renderers clamp to their
|
||||
platform limits.
|
||||
|
||||
## Adding a New Event Formatter
|
||||
|
||||
1. Add the event type to `GITHUB_COLORS` in `server/lib/formatters/colors.ts` (if new color needed)
|
||||
|
|
|
|||
|
|
@ -75,8 +75,13 @@ server/ # Nitro 服务器(H3 处理器位于 server/routes/
|
|||
└── locales/ # en.ts、zh.ts 翻译字典
|
||||
|
||||
tests/ # 单元测试 (bun test)
|
||||
tests/fixtures/ # 提供方负载固件 (github/gitea/custom)
|
||||
tests/__snapshots__/ # 格式化器快照黄金文件
|
||||
```
|
||||
|
||||
持续集成(`.github/workflows/ci.yml`)运行 `bun install --frozen-lockfile`、`bun test` 与
|
||||
`bun run lint`。CodeQL(`codeql.yml`)与 Dependabot(`dependabot.yml`)配置在 `.github/` 下。
|
||||
|
||||
## 脚本
|
||||
|
||||
| 命令 | 说明 |
|
||||
|
|
@ -112,6 +117,11 @@ bun test
|
|||
curl http://localhost:8787/health
|
||||
```
|
||||
|
||||
除各模块单元测试外,测试套件还包含提供方固件(`tests/fixtures/` +
|
||||
`tests/provider-fixtures.test.ts`)、格式化器快照(`tests/formatter-snapshot.test.ts`)以及
|
||||
平台契约测试(`tests/platform-contract.test.ts`,断言 Discord/Telegram 渲染器会截断到各自的
|
||||
平台限制)。
|
||||
|
||||
## 添加新事件格式化器
|
||||
|
||||
1. 将事件类型添加到 `server/lib/formatters/colors.ts` 中的 `GITHUB_COLORS`(如果需要新颜色)
|
||||
|
|
|
|||
84
tests/__snapshots__/formatter-snapshot.test.ts.snap
Normal file
84
tests/__snapshots__/formatter-snapshot.test.ts.snap
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
|
||||
|
||||
exports[`formatter snapshots push 1`] = `
|
||||
{
|
||||
"author": {
|
||||
"iconUrl": undefined,
|
||||
"name": "octocat",
|
||||
"url": "https://github.com/octocat",
|
||||
},
|
||||
"color": 3056719,
|
||||
"description": "[View comparison](https://github.com/acme/widget/compare/aaa...bbb)",
|
||||
"fields": [
|
||||
{
|
||||
"inline": false,
|
||||
"name": "",
|
||||
"value": "[\`bbbbbbb\`](https://github.com/acme/widget/commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb) Fix login bug",
|
||||
},
|
||||
],
|
||||
"footer": "acme/widget",
|
||||
"timestamp": "TIMESTAMP",
|
||||
"title": "acme/widget: Pushed 1 commit to [\`main\`](https://github.com/acme/widget/tree/main)",
|
||||
"url": "https://github.com/acme/widget/compare/aaa...bbb",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`formatter snapshots pull_request 1`] = `
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"id": "ghpr|merge|acme|widget|7",
|
||||
"label": "Merge",
|
||||
"style": "primary",
|
||||
},
|
||||
{
|
||||
"id": "ghpr|close|acme|widget|7",
|
||||
"label": "Close",
|
||||
"style": "danger",
|
||||
},
|
||||
],
|
||||
"author": {
|
||||
"iconUrl": undefined,
|
||||
"name": "octocat",
|
||||
"url": "https://github.com/octocat",
|
||||
},
|
||||
"color": 2991182,
|
||||
"description":
|
||||
"🟢 **Opened** pull request
|
||||
|
||||
Implements the new feature."
|
||||
,
|
||||
"fields": [
|
||||
{
|
||||
"inline": true,
|
||||
"name": "Branch",
|
||||
"value": "[\`feature\`](https://github.com/acme/widget/tree/feature) → [\`main\`](https://github.com/acme/widget/tree/main)",
|
||||
},
|
||||
],
|
||||
"footer": "acme/widget",
|
||||
"timestamp": "TIMESTAMP",
|
||||
"title": "acme/widget#7: Add feature",
|
||||
"url": "https://github.com/acme/widget/pull/7",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`formatter snapshots issues 1`] = `
|
||||
{
|
||||
"author": {
|
||||
"iconUrl": undefined,
|
||||
"name": "octocat",
|
||||
"url": "https://github.com/octocat",
|
||||
},
|
||||
"color": 2991182,
|
||||
"description":
|
||||
"🟢 **Opened** issue
|
||||
|
||||
Login fails on iOS Safari."
|
||||
,
|
||||
"fields": undefined,
|
||||
"footer": "acme/widget",
|
||||
"timestamp": "TIMESTAMP",
|
||||
"title": "acme/widget#12: Broken login on mobile",
|
||||
"url": "https://github.com/acme/widget/issues/12",
|
||||
}
|
||||
`;
|
||||
9
tests/fixtures/custom.ts
vendored
Normal file
9
tests/fixtures/custom.ts
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const customPayload: Record<string, unknown> = {
|
||||
deliveryId: "custom-123",
|
||||
title: "Deploy finished",
|
||||
repository: {
|
||||
full_name: "acme/widget",
|
||||
html_url: "https://github.com/acme/widget",
|
||||
},
|
||||
message: "The latest build was deployed to staging.",
|
||||
};
|
||||
20
tests/fixtures/gitea.ts
vendored
Normal file
20
tests/fixtures/gitea.ts
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export const giteaPush: Record<string, unknown> = {
|
||||
ref: "refs/heads/main",
|
||||
before: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
after: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
compare_url: "https://git.example.com/acme/widget/compare/aaa...bbb",
|
||||
commits: [
|
||||
{
|
||||
id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
message: "Fix login bug",
|
||||
url: "https://git.example.com/acme/widget/commit/bbb",
|
||||
timestamp: "2026-08-15T00:00:00Z",
|
||||
author: { name: "Octocat", username: "octocat" },
|
||||
},
|
||||
],
|
||||
repository: {
|
||||
full_name: "acme/widget",
|
||||
html_url: "https://git.example.com/acme/widget",
|
||||
},
|
||||
pusher: { login: "octocat" },
|
||||
};
|
||||
66
tests/fixtures/github.ts
vendored
Normal file
66
tests/fixtures/github.ts
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
export const githubPush: Record<string, unknown> = {
|
||||
ref: "refs/heads/main",
|
||||
before: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
after: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
compare: "https://github.com/acme/widget/compare/aaa...bbb",
|
||||
head_commit: {
|
||||
id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
message: "Fix login bug",
|
||||
timestamp: "2026-08-15T00:00:00Z",
|
||||
url: "https://github.com/acme/widget/commit/bbb",
|
||||
author: { name: "Octocat", username: "octocat" },
|
||||
},
|
||||
repository: {
|
||||
full_name: "acme/widget",
|
||||
html_url: "https://github.com/acme/widget",
|
||||
},
|
||||
sender: { login: "octocat" },
|
||||
pusher: { name: "octocat" },
|
||||
commits: [
|
||||
{
|
||||
id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
message: "Fix login bug",
|
||||
url: "https://github.com/acme/widget/commit/bbb",
|
||||
timestamp: "2026-08-15T00:00:00Z",
|
||||
author: { name: "Octocat", username: "octocat" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const githubPullRequest: Record<string, unknown> = {
|
||||
action: "opened",
|
||||
number: 7,
|
||||
pull_request: {
|
||||
number: 7,
|
||||
title: "Add feature",
|
||||
html_url: "https://github.com/acme/widget/pull/7",
|
||||
state: "open",
|
||||
user: { login: "octocat" },
|
||||
body: "Implements the new feature.",
|
||||
head: { ref: "feature" },
|
||||
base: { ref: "main" },
|
||||
merged: false,
|
||||
},
|
||||
repository: {
|
||||
full_name: "acme/widget",
|
||||
html_url: "https://github.com/acme/widget",
|
||||
},
|
||||
sender: { login: "octocat" },
|
||||
};
|
||||
|
||||
export const githubIssues: Record<string, unknown> = {
|
||||
action: "opened",
|
||||
issue: {
|
||||
number: 12,
|
||||
title: "Broken login on mobile",
|
||||
html_url: "https://github.com/acme/widget/issues/12",
|
||||
state: "open",
|
||||
user: { login: "octocat" },
|
||||
body: "Login fails on iOS Safari.",
|
||||
},
|
||||
repository: {
|
||||
full_name: "acme/widget",
|
||||
html_url: "https://github.com/acme/widget",
|
||||
},
|
||||
sender: { login: "octocat" },
|
||||
};
|
||||
3
tests/fixtures/index.ts
vendored
Normal file
3
tests/fixtures/index.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { githubPush, githubPullRequest, githubIssues } from "./github";
|
||||
export { giteaPush } from "./gitea";
|
||||
export { customPayload } from "./custom";
|
||||
34
tests/formatter-snapshot.test.ts
Normal file
34
tests/formatter-snapshot.test.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { describe, test, expect } from "bun:test";
|
||||
import { formatEvent } from "../server/lib/formatters";
|
||||
import type { NeutralMessage, Route, WebhookEvent } from "../server/lib/types";
|
||||
import { githubPush, githubPullRequest, githubIssues } from "./fixtures";
|
||||
|
||||
const route: Route = {
|
||||
id: "test",
|
||||
name: "Test",
|
||||
enabled: true,
|
||||
filters: [],
|
||||
targets: [{ channelId: "111" }],
|
||||
};
|
||||
|
||||
function event(ev: string, payload: Record<string, unknown>): WebhookEvent {
|
||||
return { event: ev, payload };
|
||||
}
|
||||
|
||||
function stable(msg: NeutralMessage): NeutralMessage {
|
||||
return { ...msg, timestamp: msg.timestamp ? "TIMESTAMP" : msg.timestamp };
|
||||
}
|
||||
|
||||
describe("formatter snapshots", () => {
|
||||
test("push", () => {
|
||||
expect(stable(formatEvent(route, event("push", githubPush)))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("pull_request", () => {
|
||||
expect(stable(formatEvent(route, event("pull_request", githubPullRequest)))).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("issues", () => {
|
||||
expect(stable(formatEvent(route, event("issues", githubIssues)))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
44
tests/platform-contract.test.ts
Normal file
44
tests/platform-contract.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { describe, test, expect } from "bun:test";
|
||||
import { renderNeutralMessage as renderDiscord } from "../server/lib/drivers/discord/render";
|
||||
import { renderNeutralMessage as renderTelegram } from "../server/lib/drivers/telegram/render";
|
||||
import {
|
||||
MAX_TITLE,
|
||||
MAX_DESCRIPTION,
|
||||
MAX_FIELDS,
|
||||
MAX_FIELD_VALUE,
|
||||
MAX_FOOTER,
|
||||
} from "../server/lib/formatters/helpers";
|
||||
import type { NeutralMessage } from "../server/lib/types";
|
||||
|
||||
const big = "x".repeat(10000);
|
||||
|
||||
function oversized(): NeutralMessage {
|
||||
return {
|
||||
title: `acme/widget#1: ${big}`,
|
||||
url: "https://github.com/acme/widget",
|
||||
description: big,
|
||||
fields: Array.from({ length: 30 }, (_, i) => ({ name: `field-${i}`, value: big })),
|
||||
footer: big,
|
||||
};
|
||||
}
|
||||
|
||||
describe("platform contract", () => {
|
||||
test("discord render clamps all limits", () => {
|
||||
const embed = renderDiscord(oversized()).embeds?.[0];
|
||||
expect(embed).toBeDefined();
|
||||
expect(embed!.title!.length).toBeLessThanOrEqual(MAX_TITLE);
|
||||
expect(embed!.description!.length).toBeLessThanOrEqual(MAX_DESCRIPTION);
|
||||
expect(embed!.fields!.length).toBeLessThanOrEqual(MAX_FIELDS);
|
||||
for (const f of embed!.fields!) {
|
||||
expect(f.value.length).toBeLessThanOrEqual(MAX_FIELD_VALUE);
|
||||
}
|
||||
expect(embed!.footer).toBeDefined();
|
||||
expect(embed!.footer!.text.length).toBeLessThanOrEqual(MAX_FOOTER);
|
||||
});
|
||||
|
||||
test("telegram render stays within the 4096-char cap", () => {
|
||||
const html = renderTelegram(oversized());
|
||||
expect(html.length).toBeGreaterThan(4000);
|
||||
expect(html.length).toBeLessThan(4200);
|
||||
});
|
||||
});
|
||||
55
tests/provider-fixtures.test.ts
Normal file
55
tests/provider-fixtures.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { describe, test, expect } from "bun:test";
|
||||
import { formatEvent } from "../server/lib/formatters";
|
||||
import { detectProvider } from "../server/lib/providers";
|
||||
import type { Route, WebhookEvent } from "../server/lib/types";
|
||||
import { githubPush, githubPullRequest, githubIssues, giteaPush, customPayload } from "./fixtures";
|
||||
|
||||
const route: Route = {
|
||||
id: "test",
|
||||
name: "Test",
|
||||
enabled: true,
|
||||
filters: [],
|
||||
targets: [{ channelId: "111" }],
|
||||
};
|
||||
|
||||
function event(ev: string, payload: Record<string, unknown>): WebhookEvent {
|
||||
return { event: ev, payload };
|
||||
}
|
||||
|
||||
describe("provider fixtures", () => {
|
||||
const githubFixtures = [
|
||||
{ event: "push", payload: githubPush },
|
||||
{ event: "pull_request", payload: githubPullRequest },
|
||||
{ event: "issues", payload: githubIssues },
|
||||
];
|
||||
|
||||
for (const { event: ev, payload } of githubFixtures) {
|
||||
test(`formats github ${ev} with a title`, () => {
|
||||
const msg = formatEvent(route, event(ev, payload));
|
||||
expect(msg.title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test(`github parse detects ${ev}`, () => {
|
||||
const headers = { "x-github-event": ev };
|
||||
const parsed = detectProvider(headers)?.parse(JSON.stringify(payload), headers);
|
||||
expect(parsed?.event).toBe(ev);
|
||||
});
|
||||
}
|
||||
|
||||
test("formats gitea push with a title", () => {
|
||||
const msg = formatEvent(route, event("push", giteaPush));
|
||||
expect(msg.title.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("gitea parse detects push", () => {
|
||||
const headers = { "x-gitea-event": "push" };
|
||||
const parsed = detectProvider(headers)?.parse(JSON.stringify(giteaPush), headers);
|
||||
expect(parsed?.event).toBe("push");
|
||||
});
|
||||
|
||||
test("custom parse detects custom event", () => {
|
||||
const headers = { "x-webhooker-signature": "sha256=abc" };
|
||||
const parsed = detectProvider(headers)?.parse(JSON.stringify(customPayload), headers);
|
||||
expect(parsed?.event).toBe("custom");
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,9 @@
|
|||
"name": "webhooker",
|
||||
"main": ".output/server/index.mjs",
|
||||
"compatibility_date": "2026-08-04",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"compatibility_flags": [
|
||||
"nodejs_compat"
|
||||
],
|
||||
"build": {
|
||||
"command": "bun run build"
|
||||
},
|
||||
|
|
@ -32,8 +34,8 @@
|
|||
"queues": {
|
||||
"producers": [
|
||||
{
|
||||
"queue": "webhooker-delivery",
|
||||
"binding": "QUEUE",
|
||||
"queue": "webhooker-delivery"
|
||||
}
|
||||
],
|
||||
"consumers": [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue