chore: auto-fix lint & formatting [skip ci]

This commit is contained in:
github-actions[bot] 2026-08-13 12:37:09 +00:00
parent 41ad1a036b
commit df51b71121
22 changed files with 1419 additions and 1408 deletions

View file

@ -258,12 +258,7 @@ if (view.value === null) {
throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: false }); throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: false });
} }
const { const { logs, loading: logsLoading, error: logsError, load: loadLogs } = useSendLogs();
logs,
loading: logsLoading,
error: logsError,
load: loadLogs,
} = useSendLogs();
const { const {
entries: auditEntries, entries: auditEntries,
loading: auditLoading, loading: auditLoading,

View file

@ -322,4 +322,3 @@ function save(): void {
}); });
} }
</script> </script>

View file

@ -57,7 +57,10 @@ const updated = computed(() => "2026-08-01");
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en); const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
const q = (p: string): string => `${p}?lang=${lang.value}`; const q = (p: string): string => `${p}?lang=${lang.value}`;
const altLink = computed(() => const altLink = computed(() =>
q(props.active === "terms" ? "/terms" : "/privacy").replace(`lang=${lang.value}`, `lang=${altLang.value}`), q(props.active === "terms" ? "/terms" : "/privacy").replace(
`lang=${lang.value}`,
`lang=${altLang.value}`,
),
); );
useHead({ title: `${props.title} · WebHooker` }); useHead({ title: `${props.title} · WebHooker` });

View file

@ -22,7 +22,11 @@
v-for="it in items" v-for="it in items"
:key="it.label" :key="it.label"
class="flex items-center gap-4 rounded-[14px] border border-border bg-surface px-5 py-[18px] text-text no-underline shadow-card transition-all duration-150 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-card-hover" class="flex items-center gap-4 rounded-[14px] border border-border bg-surface px-5 py-[18px] text-text no-underline shadow-card transition-all duration-150 hover:-translate-y-0.5 hover:border-border-strong hover:shadow-card-hover"
:class="it.primary ? 'border-accent bg-accent shadow-none hover:border-accent hover:shadow-accent-lg' : ''" :class="
it.primary
? 'border-accent bg-accent shadow-none hover:border-accent hover:shadow-accent-lg'
: ''
"
:href="it.href" :href="it.href"
:target="it.external ? '_blank' : undefined" :target="it.external ? '_blank' : undefined"
:rel="it.external ? 'noopener noreferrer' : undefined" :rel="it.external ? 'noopener noreferrer' : undefined"
@ -68,7 +72,9 @@ const config = useRuntimeConfig();
const lang = computed(() => (route.query.lang === "en" ? "en" : "zh")); const lang = computed(() => (route.query.lang === "en" ? "en" : "zh"));
const altLang = computed(() => (lang.value === "zh" ? "en" : "zh")); const altLang = computed(() => (lang.value === "zh" ? "en" : "zh"));
const repo = computed(() => (config.public.repoUrl as string) || DEFAULT_REPO); const repo = computed(() => (config.public.repoUrl as string) || DEFAULT_REPO);
const docsBase = computed(() => ((config.public.docsUrl as string) || DEFAULT_DOCS).replace(/\/+$/, "")); const docsBase = computed(() =>
((config.public.docsUrl as string) || DEFAULT_DOCS).replace(/\/+$/, ""),
);
const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en); const t = (zh: string, en: string): string => (lang.value === "zh" ? zh : en);
@ -119,7 +125,8 @@ const ICON_PATHS: Record<string, string> = {
docs: '<path d="M4 4a2 2 0 0 1 2-2h7l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z"/><path d="M13 2v5h5"/><path d="M8 12h8M8 16h6"/>', docs: '<path d="M4 4a2 2 0 0 1 2-2h7l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4z"/><path d="M13 2v5h5"/><path d="M8 12h8M8 16h6"/>',
github: github:
'<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.36 1.09 2.94.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2z"/>', '<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.36 1.09 2.94.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2z"/>',
terms: '<path d="M9 12h6M9 16h6M9 8h2"/><path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"/>', terms:
'<path d="M9 12h6M9 16h6M9 8h2"/><path d="M6 2h9l5 5v13a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z"/>',
privacy: privacy:
'<path d="M12 2l7 3v6c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V5l7-3z"/><path d="M9 12l2 2 4-4"/>', '<path d="M12 2l7 3v6c0 5-3.5 8.5-7 10-3.5-1.5-7-5-7-10V5l7-3z"/><path d="M9 12l2 2 4-4"/>',
login: login:
@ -143,4 +150,3 @@ useHead({
], ],
}); });
</script> </script>

View file

@ -26,14 +26,14 @@ The route above matches both `push` and `pull_request` events.
Every filter type shares the same three pattern forms: Every filter type shares the same three pattern forms:
| Pattern | Meaning | | Pattern | Meaning |
| ---------------------- | -------------------------------------------------------------- | | ---------------------- | ------------------------------------------------------------- |
| `plain text` | Field filters: **exact** match. `keyword`: search anywhere. | | `plain text` | Field filters: **exact** match. `keyword`: search anywhere. |
| `*` / `?` | **Glob wildcards**`*` any run, `?` one char. | | `*` / `?` | **Glob wildcards**`*` any run, `?` one char. |
| `/regular expression/` | Compiled as a **regular expression** (case-insensitive flag). | | `/regular expression/` | Compiled as a **regular expression** (case-insensitive flag). |
- On field filters (`event`/`repo`/`actor`/`action`/`branch`), plain text and globs match the whole value; on `keyword` they search anywhere in the payload. - On field filters (`event`/`repo`/`actor`/`action`/`branch`), plain text and globs match the whole value; on `keyword` they search anywhere in the payload.
- Regexes always search: `/^feat/` matches values *starting* with `feat`, `/feat/` matches anywhere. - Regexes always search: `/^feat/` matches values _starting_ with `feat`, `/feat/` matches anywhere.
Examples: Examples:
@ -259,5 +259,5 @@ Forward workflow runs that ended in failure on any branch, to a `#ci` channel:
- **A `//`-wrapped invalid regex never matches.** Unlike plain text, an unwrapped invalid pattern is matched literally — wrap patterns only when they are real regular expressions. - **A `//`-wrapped invalid regex never matches.** Unlike plain text, an unwrapped invalid pattern is matched literally — wrap patterns only when they are real regular expressions.
- **An `action` filter on an action-less event never matches.** Check the event has an `action` field first (see [Filter Compatibility](../events/supported#filter-compatibility)). - **An `action` filter on an action-less event never matches.** Check the event has an `action` field first (see [Filter Compatibility](../events/supported#filter-compatibility)).
- **`branch` on an event without a branch never matches.** A `branch` filter on an `issues` event will always be false. Use `keyword` if you need branch-like matching there. - **`branch` on an event without a branch never matches.** A `branch` filter on an `issues` event will always be false. Use `keyword` if you need branch-like matching there.
- **`keyword` searches everything.** Because it scans the whole payload, a pattern like `"fix"` can match commit messages, issue titles, *and* repository names. Be as specific as possible. - **`keyword` searches everything.** Because it scans the whole payload, a pattern like `"fix"` can match commit messages, issue titles, _and_ repository names. Be as specific as possible.
- **Forgetting `exclude` semantics.** `exclude: true` negates the whole filter — one non-matching value in an array does not "block" the route; the negated filter matches only when *none* of the values match. - **Forgetting `exclude` semantics.** `exclude: true` negates the whole filter — one non-matching value in an array does not "block" the route; the negated filter matches only when _none_ of the values match.

View file

@ -63,7 +63,7 @@ curl http://localhost:8787/health
## Available Scripts ## Available Scripts
| Script | Description | | Script | Description |
|------------------------|---------------------------------------------| | ---------------------- | ------------------------------------------- |
| `bun run dev` | Start Nuxt dev server (HMR) | | `bun run dev` | Start Nuxt dev server (HMR) |
| `bun run build` | Production build (cloudflare_module preset) | | `bun run build` | Production build (cloudflare_module preset) |
| `bun run deploy` | Deploy to Cloudflare | | `bun run deploy` | Deploy to Cloudflare |

View file

@ -296,14 +296,14 @@ owner及超级管理员可在分组的「成员」面板创建一次性邀
实操指南见[过滤器教程](./filters),包含完整示例。 实操指南见[过滤器教程](./filters),包含完整示例。
| 类型 | 匹配对象 | 示例 | | 类型 | 匹配对象 | 示例 |
| --------- | ---------------- | ----------------------------------------- | | --------- | ---------------- | ------------------------------------- |
| `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` | | `event` | GitHub 事件名称 | `push`, `pull_*`, `pull_request` |
| `repo` | 仓库全名 | `org/repo`, `org/*` | | `repo` | 仓库全名 | `org/repo`, `org/*` |
| `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` | | `actor` | 发送者登录名 | `username`, `[bot]`, `*[bot]` |
| `action` | 事件操作 | `opened`, `closed`, `published` | | `action` | 事件操作 | `opened`, `closed`, `published` |
| `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` | | `branch` | 分支名称 | `main`, `feature-?`, `/^release-/` |
| `keyword` | 载荷正文中的文本 | `deploy`, `*release-*`, `/fix\s+\d+/` | | `keyword` | 载荷正文中的文本 | `deploy`, `*release-*`, `/fix\s+\d+/` |
### 过滤器行为 ### 过滤器行为

View file

@ -26,11 +26,11 @@
所有过滤器类型共享以下三种模式写法: 所有过滤器类型共享以下三种模式写法:
| 模式 | 含义 | | 模式 | 含义 |
| -------------- | ----------------------------------------------------------------- | | -------------- | --------------------------------------------------------------- |
| `纯文本` | 字段过滤器:**完全相等**匹配;`keyword`:在载荷中任意位置搜索。 | | `纯文本` | 字段过滤器:**完全相等**匹配;`keyword`:在载荷中任意位置搜索。 |
| `*` / `?` | **通配符glob**——`*` 任意长度、`?` 恰好一个字符。 | | `*` / `?` | **通配符glob**——`*` 任意长度、`?` 恰好一个字符。 |
| `/正则表达式/` | 按**正则表达式**编译(忽略大小写标志)。 | | `/正则表达式/` | 按**正则表达式**编译(忽略大小写标志)。 |
- 字段过滤器(`event`/`repo`/`actor`/`action`/`branch`)的纯文本与通配符匹配整个值;`keyword` 则在载荷中任意位置搜索。 - 字段过滤器(`event`/`repo`/`actor`/`action`/`branch`)的纯文本与通配符匹配整个值;`keyword` 则在载荷中任意位置搜索。
- 正则表达式始终是搜索语义:`/^feat/` 匹配以 `feat` **开头**的值,`/feat/` 匹配任意位置出现 `feat` 的值。 - 正则表达式始终是搜索语义:`/^feat/` 匹配以 `feat` **开头**的值,`/feat/` 匹配任意位置出现 `feat` 的值。

View file

@ -1,6 +1,4 @@
import defaultNitroErrorHandler, { import defaultNitroErrorHandler, { defineNitroErrorHandler } from "nitropack/runtime/error";
defineNitroErrorHandler,
} from "nitropack/runtime/error";
import { setResponseStatus } from "h3"; import { setResponseStatus } from "h3";
/** /**

View file

@ -21,7 +21,10 @@ async function readJsonBody(event: H3Event): Promise<Record<string, unknown> | n
} }
} }
async function userOctokit(event: H3Event, userId: string): Promise<Awaited<ReturnType<typeof getUserOctokit>>> { async function userOctokit(
event: H3Event,
userId: string,
): Promise<Awaited<ReturnType<typeof getUserOctokit>>> {
return getUserOctokit(userId, cfEnv(event).KV); return getUserOctokit(userId, cfEnv(event).KV);
} }
@ -123,16 +126,7 @@ export async function apiClose(event: H3Event): Promise<Record<string, unknown>>
export async function apiReact(event: H3Event): Promise<Record<string, unknown>> { export async function apiReact(event: H3Event): Promise<Record<string, unknown>> {
const userId = await bearerUserId(event); const userId = await bearerUserId(event);
const body = await readJsonBody(event); const body = await readJsonBody(event);
const reactions = [ const reactions = ["+1", "-1", "laugh", "confused", "heart", "hooray", "rocket", "eyes"] as const;
"+1",
"-1",
"laugh",
"confused",
"heart",
"hooray",
"rocket",
"eyes",
] as const;
if ( if (
!body || !body ||
!isNonEmptyString(body.owner) || !isNonEmptyString(body.owner) ||
@ -151,14 +145,7 @@ export async function apiReact(event: H3Event): Promise<Record<string, unknown>>
repo: body.repo, repo: body.repo,
issue_number: body.issueNumber, issue_number: body.issueNumber,
content: body.reaction as content: body.reaction as
| "+1" "+1" | "-1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes",
| "-1"
| "laugh"
| "confused"
| "heart"
| "hooray"
| "rocket"
| "eyes",
}); });
} catch (err) { } catch (err) {
log.error({ err }, "Failed to create reaction"); log.error({ err }, "Failed to create reaction");

View file

@ -402,7 +402,10 @@ export async function adminInvite(event: H3Event): Promise<void> {
} }
const session = await getAdminSession(env.KV, getHeader(event, "cookie")); const session = await getAdminSession(env.KV, getHeader(event, "cookie"));
if (!session) { if (!session) {
await sendRedirect(event, `/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`); await sendRedirect(
event,
`/auth/github?redirect=${encodeURIComponent(`/admin/invite?token=${token}`)}`,
);
return; return;
} }
const result = await acceptInvite(env.KV, token, session.userId, session.login); const result = await acceptInvite(env.KV, token, session.userId, session.login);
@ -480,8 +483,7 @@ export async function adminApiGroupsPut(event: H3Event): Promise<Record<string,
const members = g.members ?? normalizeGroupMembers(g); const members = g.members ?? normalizeGroupMembers(g);
const stillMine = members.some( const stillMine = members.some(
(m) => (m) =>
m.role === "owner" && m.role === "owner" && identityMatches([m.login], auth.session.userId, auth.session.login),
identityMatches([m.login], auth.session.userId, auth.session.login),
); );
const otherOwner = ownerCount(members) > 1; const otherOwner = ownerCount(members) > 1;
if (!stillMine && !otherOwner) { if (!stillMine && !otherOwner) {
@ -648,7 +650,10 @@ export async function adminApiLogs(event: H3Event): Promise<Record<string, unkno
} }
/** GET /admin/api/logs/:id */ /** GET /admin/api/logs/:id */
export async function adminApiLogsById(event: H3Event, id: number): Promise<Record<string, unknown>> { export async function adminApiLogsById(
event: H3Event,
id: number,
): Promise<Record<string, unknown>> {
const auth = await requireAnyAccess(event); const auth = await requireAnyAccess(event);
const env = cfEnv(event); const env = cfEnv(event);
if (!Number.isInteger(id) || id <= 0) return respondError(event, 400, "Invalid log id"); if (!Number.isInteger(id) || id <= 0) return respondError(event, 400, "Invalid log id");
@ -783,7 +788,10 @@ export async function adminGroupInvitesGet(
} }
/** DELETE /admin/api/invites/:token */ /** DELETE /admin/api/invites/:token */
export async function adminInviteDelete(event: H3Event, token: string): Promise<Record<string, unknown>> { export async function adminInviteDelete(
event: H3Event,
token: string,
): Promise<Record<string, unknown>> {
await requireAnyAccess(event); await requireAnyAccess(event);
const env = cfEnv(event); const env = cfEnv(event);
const invite = await getInvite(env.KV, token); const invite = await getInvite(env.KV, token);

View file

@ -64,11 +64,7 @@ export function requireGroup(event: H3Event, groupId: string): GroupAccess {
} }
/** Requires at least `min` role in the group (owner|admin|viewer). */ /** Requires at least `min` role in the group (owner|admin|viewer). */
export function requireGroupRole( export function requireGroupRole(event: H3Event, groupId: string, min: GroupRole): GroupAccess {
event: H3Event,
groupId: string,
min: GroupRole,
): GroupAccess {
const access = requireGroup(event, groupId); const access = requireGroup(event, groupId);
if (!access.ok) return access; if (!access.ok) return access;
const auth = currentAuth(event); const auth = currentAuth(event);
@ -98,7 +94,6 @@ export async function bearerUserId(event: H3Event): Promise<string> {
if (!auth?.startsWith("Bearer ")) if (!auth?.startsWith("Bearer "))
throw createError({ statusCode: 401, statusMessage: "Missing authorization" }); throw createError({ statusCode: 401, statusMessage: "Missing authorization" });
const userId = await findUserIdByToken(env.KV, auth.slice(7)); const userId = await findUserIdByToken(env.KV, auth.slice(7));
if (!userId) if (!userId) throw createError({ statusCode: 401, statusMessage: "Invalid or expired token" });
throw createError({ statusCode: 401, statusMessage: "Invalid or expired token" });
return userId; return userId;
} }

View file

@ -157,8 +157,11 @@ export async function handleInstallPage(event: H3Event): Promise<string | void>
return; return;
} }
const accountLogin = const accountLogin =
(await getInstallationAccount(env.GITHUB_APP_ID ?? "", env.GITHUB_PRIVATE_KEY ?? "", installationId)) ?? (await getInstallationAccount(
""; env.GITHUB_APP_ID ?? "",
env.GITHUB_PRIVATE_KEY ?? "",
installationId,
)) ?? "";
const groups = await loadGroups(env.KV); const groups = await loadGroups(env.KV);
const scope = resolveScope(env, groups, session.userId, session.login); const scope = resolveScope(env, groups, session.userId, session.login);
const owned = groups.filter((g) => roleAt(scope, g.id) === "owner"); const owned = groups.filter((g) => roleAt(scope, g.id) === "owner");
@ -216,8 +219,11 @@ export async function handleInstallBind(event: H3Event): Promise<void> {
// Default: auto-create a dedicated inst-{id} group. // Default: auto-create a dedicated inst-{id} group.
const accountLogin = const accountLogin =
(await getInstallationAccount(env.GITHUB_APP_ID ?? "", env.GITHUB_PRIVATE_KEY ?? "", installationId)) ?? (await getInstallationAccount(
""; env.GITHUB_APP_ID ?? "",
env.GITHUB_PRIVATE_KEY ?? "",
installationId,
)) ?? "";
const group = await ensureInstallationGroup(env.KV, installationId, accountLogin); const group = await ensureInstallationGroup(env.KV, installationId, accountLogin);
if (!group) { if (!group) {
await sendRedirect(event, "/admin?error=install"); await sendRedirect(event, "/admin?error=install");
@ -248,7 +254,10 @@ export async function handleInstallBind(event: H3Event): Promise<void> {
adminIds: [...new Set([...(group.adminIds ?? []), session.login])], adminIds: [...new Set([...(group.adminIds ?? []), session.login])],
}; };
const all = await loadGroups(env.KV); const all = await loadGroups(env.KV);
await saveGroups(env.KV, all.map((g) => (g.id === group.id ? updated : g))); await saveGroups(
env.KV,
all.map((g) => (g.id === group.id ? updated : g)),
);
await recordAudit(env.DB, { await recordAudit(env.DB, {
ts: Date.now(), ts: Date.now(),
actorId: session.userId, actorId: session.userId,

View file

@ -117,10 +117,7 @@ export async function processWebhook(
} }
/** h3 wrapper for `POST /webhook` / `POST /webhook/:groupId`. */ /** h3 wrapper for `POST /webhook` / `POST /webhook/:groupId`. */
export async function handleWebhookRequest( export async function handleWebhookRequest(event: H3Event, tenantId?: string): Promise<unknown> {
event: H3Event,
tenantId?: string,
): Promise<unknown> {
const contentLength = Number(getHeader(event, "content-length") ?? 0); const contentLength = Number(getHeader(event, "content-length") ?? 0);
if (contentLength > MAX_BODY_SIZE) { if (contentLength > MAX_BODY_SIZE) {
setResponseStatus(event, 413); setResponseStatus(event, 413);

View file

@ -47,8 +47,8 @@ export default <Partial<Config>>{
card: "var(--shadow)", card: "var(--shadow)",
"card-hover": "0 8px 24px -12px rgba(15, 23, 42, 0.25)", "card-hover": "0 8px 24px -12px rgba(15, 23, 42, 0.25)",
"accent-lg": "0 12px 28px -10px var(--accent)", "accent-lg": "0 12px 28px -10px var(--accent)",
"drawer": "-16px 0 48px rgba(15, 23, 42, 0.12)", drawer: "-16px 0 48px rgba(15, 23, 42, 0.12)",
"modal": "0 12px 40px rgba(15, 23, 42, 0.25)", modal: "0 12px 40px rgba(15, 23, 42, 0.25)",
}, },
keyframes: { keyframes: {
rise: { rise: {

View file

@ -1,9 +1,5 @@
import { describe, it, expect } from "bun:test"; import { describe, it, expect } from "bun:test";
import { import { adminGroupRename, adminGroupRoutesGet, adminApiMe } from "../server/lib/web/admin";
adminGroupRename,
adminGroupRoutesGet,
adminApiMe,
} from "../server/lib/web/admin";
import { createAdminSession, adminCookie } from "../server/lib/web/session"; import { createAdminSession, adminCookie } from "../server/lib/web/session";
import { loadGroups } from "../server/lib/web/groups"; import { loadGroups } from "../server/lib/web/groups";
import { loadRoutes } from "../server/lib/config"; import { loadRoutes } from "../server/lib/config";

View file

@ -34,7 +34,9 @@ describe("message title spec", () => {
expect(msg.title).toBe( expect(msg.title).toBe(
"acme/widget: Pushed 1 commit to [`main`](https://github.com/acme/widget/tree/main)", "acme/widget: Pushed 1 commit to [`main`](https://github.com/acme/widget/tree/main)",
); );
expect(msg.description).toBe("[View comparison](https://github.com/acme/widget/compare/abc...def)"); expect(msg.description).toBe(
"[View comparison](https://github.com/acme/widget/compare/abc...def)",
);
}); });
it("pull_request title is repo#number: title", () => { it("pull_request title is repo#number: title", () => {
@ -101,7 +103,9 @@ describe("message title spec", () => {
sender, sender,
}), }),
); );
expect(msg.title).toBe("acme/widget: [CI — success](https://github.com/acme/widget/actions/runs/42)"); expect(msg.title).toBe(
"acme/widget: [CI — success](https://github.com/acme/widget/actions/runs/42)",
);
expect(msg.fields![1].value).toBe("✅ build"); expect(msg.fields![1].value).toBe("✅ build");
}); });
@ -122,7 +126,9 @@ describe("message title spec", () => {
sender, sender,
}), }),
); );
expect(queued.title).toBe("acme/widget: [CI — queued](https://github.com/acme/widget/actions/runs/42)"); expect(queued.title).toBe(
"acme/widget: [CI — queued](https://github.com/acme/widget/actions/runs/42)",
);
expect(queued.fields![0].value).toBe("⏳ queued"); expect(queued.fields![0].value).toBe("⏳ queued");
const running = formatEvent( const running = formatEvent(
@ -134,7 +140,9 @@ describe("message title spec", () => {
sender, sender,
}), }),
); );
expect(running.title).toBe("acme/widget: [CI — running](https://github.com/acme/widget/actions/runs/42)"); expect(running.title).toBe(
"acme/widget: [CI — running](https://github.com/acme/widget/actions/runs/42)",
);
expect(running.fields![0].value).toBe("🔄 running"); expect(running.fields![0].value).toBe("🔄 running");
}); });

View file

@ -269,7 +269,10 @@ describe("POST /auth/github/install/bind", () => {
const env = createEnv({ KV: kv }); const env = createEnv({ KV: kv });
const sessionId = await createAdminSession(kv, "1001", "alice"); const sessionId = await createAdminSession(kv, "1001", "alice");
const event = bindEvent(env, adminCookie(sessionId), { installation_id: "555", group: "theirs" }); const event = bindEvent(env, adminCookie(sessionId), {
installation_id: "555",
group: "theirs",
});
await handleInstallBind(event); await handleInstallBind(event);
expect(responseStatus(event)).toBe(302); expect(responseStatus(event)).toBe(302);
expect(responseHeader(event, "location")).toBe("/admin?error=forbidden"); expect(responseHeader(event, "location")).toBe("/admin?error=forbidden");
@ -329,7 +332,10 @@ describe("oauth misc", () => {
it("starts the OAuth flow with a state token", async () => { it("starts the OAuth flow with a state token", async () => {
const kv = createMockKV(); const kv = createMockKV();
const env = createEnv({ KV: kv, GITHUB_CLIENT_ID: "client-1" }); const env = createEnv({ KV: kv, GITHUB_CLIENT_ID: "client-1" });
const event = makeEvent("/auth/github?redirect=/admin", { headers: { accept: "text/html" }, env }); const event = makeEvent("/auth/github?redirect=/admin", {
headers: { accept: "text/html" },
env,
});
await handleOAuthStart(event); await handleOAuthStart(event);
expect(responseStatus(event)).toBe(302); expect(responseStatus(event)).toBe(302);
const location = responseHeader(event, "location") ?? ""; const location = responseHeader(event, "location") ?? "";

View file

@ -1,5 +1,11 @@
import { describe, it, expect, beforeEach } from "bun:test"; import { describe, it, expect, beforeEach } from "bun:test";
import { createInvite, getInvite, listInvites, revokeInvite, acceptInvite } from "../server/lib/web/invites"; import {
createInvite,
getInvite,
listInvites,
revokeInvite,
acceptInvite,
} from "../server/lib/web/invites";
import { saveGroups, loadGroups } from "../server/lib/web/groups"; import { saveGroups, loadGroups } from "../server/lib/web/groups";
import type { Group } from "../server/lib/types"; import type { Group } from "../server/lib/types";

View file

@ -184,22 +184,20 @@ describe("matchRoute", () => {
it("matches branch filter with single-char ? wildcard", () => { it("matches branch filter with single-char ? wildcard", () => {
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-?" }] }; const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "feature-?" }] };
expect( expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe(
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } }), true,
).toBe(true); );
expect( expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-xy" } })).toBe(
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-xy" } }), false,
).toBe(false); );
}); });
it("matches branch filter with //-wrapped regex", () => { it("matches branch filter with //-wrapped regex", () => {
const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "/^feat/" }] }; const route = { ...baseRoute, filters: [{ type: "branch" as const, match: "/^feat/" }] };
expect( expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } })).toBe(
matchRoute(route, { event: "push", payload: { ref: "refs/heads/feature-x" } }), true,
).toBe(true); );
expect( expect(matchRoute(route, { event: "push", payload: { ref: "refs/heads/main" } })).toBe(false);
matchRoute(route, { event: "push", payload: { ref: "refs/heads/main" } }),
).toBe(false);
}); });
it("treats glob special chars literally when not wrapped", () => { it("treats glob special chars literally when not wrapped", () => {