chore(quality): provider fixtures, formatter snapshots, platform contract tests, CI, CodeQL, dependabot

This commit is contained in:
RhenCloud 2026-08-15 17:30:51 +08:00
parent 04fa52db52
commit 320e825bf8
No known key found for this signature in database
GPG key ID: A574A617378C4E0B
15 changed files with 405 additions and 3 deletions

10
.github/dependabot.yml vendored Normal file
View 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
View 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
View 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

View file

@ -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,

View file

@ -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)

View file

@ -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`(如果需要新颜色)

View 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
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
export { githubPush, githubPullRequest, githubIssues } from "./github";
export { giteaPush } from "./gitea";
export { customPayload } from "./custom";

View 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();
});
});

View 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);
});
});

View 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");
});
});

View file

@ -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": [