Skip to content
File

Blob: tests/worker/webhooks/helpers.ts

typescript180 lines
1import type {
2 CreateWebhookRequest,
3 DispatchMode,
4 GetProjectWebhooksResponse,
5 ProjectDetail,
6 ProjectResponse,
7 RotateWebhookSecretResponse,
8 UpdateWebhookRequest,
9 UpdateProjectRequest,
10 UpsertWebhookResponse,
11 WebhookProvider,
12} from "@/contracts";
13 
14import {
15 authHeaders,
16 createAuthenticatedSession,
17 fetchJson,
18 seedProject,
19 seedUser,
20 type SeededProject,
21 type SeededUser,
22} from "../../helpers/runtime";
23 
24export interface OwnedProjectContext {
25 user: SeededUser;
26 sessionId: string;
27 project: SeededProject;
28}
29 
30const textToBuffer = (value: string): ArrayBuffer => new TextEncoder().encode(value).buffer as ArrayBuffer;
31 
32const toHex = (bytes: ArrayBuffer): string =>
33 Array.from(new Uint8Array(bytes))
34 .map((value) => value.toString(16).padStart(2, "0"))
35 .join("");
36 
37const signHmacSha256Hex = async (secret: string, body: string): Promise<string> => {
38 const key = await crypto.subtle.importKey("raw", textToBuffer(secret), { name: "HMAC", hash: "SHA-256" }, false, [
39 "sign",
40 ]);
41 const signature = await crypto.subtle.sign("HMAC", key, textToBuffer(body));
42 return toHex(signature);
43};
44 
45export const signGitHubPayload = async (secret: string, body: string): Promise<string> =>
46 `sha256=${await signHmacSha256Hex(secret, body)}`;
47 
48export const signGiteaPayload = async (secret: string, body: string): Promise<string> =>
49 await signHmacSha256Hex(secret, body);
50 
51export const createOwnedProjectContext = async (options?: {
52 user?: {
53 email?: string;
54 slug?: string;
55 };
56 project?: {
57 projectSlug?: string;
58 repoUrl?: string;
59 defaultBranch?: string;
60 configPath?: string;
61 name?: string;
62 dispatchMode?: DispatchMode;
63 };
64}): Promise<OwnedProjectContext> => {
65 const user = await seedUser({
66 email: options?.user?.email,
67 slug: options?.user?.slug,
68 });
69 const project = await seedProject(user, {
70 projectSlug: options?.project?.projectSlug,
71 repoUrl: options?.project?.repoUrl,
72 defaultBranch: options?.project?.defaultBranch,
73 configPath: options?.project?.configPath,
74 name: options?.project?.name,
75 dispatchMode: options?.project?.dispatchMode ?? "queue",
76 });
77 const sessionId = await createAuthenticatedSession(user.id);
78 
79 return {
80 user,
81 sessionId,
82 project,
83 };
84};
85 
86export const putWebhook = async (
87 sessionId: string,
88 projectId: string,
89 provider: WebhookProvider,
90 payload: CreateWebhookRequest | UpdateWebhookRequest,
91) =>
92 await fetchJson<UpsertWebhookResponse>(`/api/private/projects/${projectId}/webhooks/${provider}`, {
93 method: "PUT",
94 headers: authHeaders(sessionId, {
95 "content-type": "application/json; charset=utf-8",
96 }),
97 body: JSON.stringify(payload),
98 });
99 
100export const getWebhooks = async (sessionId: string, projectId: string) =>
101 await fetchJson<GetProjectWebhooksResponse>(`/api/private/projects/${projectId}/webhooks`, {
102 headers: authHeaders(sessionId),
103 });
104 
105export const rotateWebhookSecret = async (sessionId: string, projectId: string, provider: WebhookProvider) =>
106 await fetchJson<RotateWebhookSecretResponse>(
107 `/api/private/projects/${projectId}/webhooks/${provider}/rotate-secret`,
108 {
109 method: "POST",
110 headers: authHeaders(sessionId),
111 },
112 );
113 
114export const deleteWebhook = async (sessionId: string, projectId: string, provider: WebhookProvider) =>
115 await fetchJson(`/api/private/projects/${projectId}/webhooks/${provider}`, {
116 method: "DELETE",
117 headers: authHeaders(sessionId),
118 });
119 
120export const patchProject = async (sessionId: string, projectId: string, payload: UpdateProjectRequest) =>
121 await fetchJson<ProjectResponse>(`/api/private/projects/${projectId}`, {
122 method: "PATCH",
123 headers: authHeaders(sessionId, {
124 "content-type": "application/json; charset=utf-8",
125 }),
126 body: JSON.stringify(payload),
127 });
128 
129export const getProjectDetail = async (sessionId: string, projectId: string) =>
130 await fetchJson<ProjectDetail>(`/api/private/projects/${projectId}`, {
131 headers: authHeaders(sessionId),
132 });
133 
134export const postPublicWebhook = async <T>(
135 provider: WebhookProvider,
136 project: Pick<SeededProject, "ownerSlug" | "projectSlug">,
137 body: string,
138 headers: HeadersInit,
139) =>
140 await fetchJson<T>(`/api/public/hooks/${provider}/${project.ownerSlug}/${project.projectSlug}`, {
141 method: "POST",
142 headers,
143 body,
144 });
145 
146export const toGitHubFullName = (repositoryUrl: string): string => {
147 const url = new URL(repositoryUrl);
148 return url.pathname.replace(/^\/+/u, "");
149};
150 
151export const buildGitHubRepository = (repositoryUrl: string, defaultBranch = "main") => {
152 const fullName = toGitHubFullName(repositoryUrl);
153 
154 return {
155 full_name: fullName,
156 html_url: repositoryUrl,
157 clone_url: `${repositoryUrl}.git`,
158 default_branch: defaultBranch,
159 };
160};
161 
162export const buildGitLabProjectPayload = (
163 repositoryUrl: string,
164 defaultBranch = "main",
165): {
166 default_branch: string;
167 git_http_url: string;
168 http_url: string;
169 path_with_namespace: string;
170} => ({
171 default_branch: defaultBranch,
172 git_http_url: `${repositoryUrl}.git`,
173 http_url: repositoryUrl,
174 path_with_namespace: new URL(repositoryUrl).pathname.replace(/^\/+/u, ""),
175});
176 
177export const buildGitLabRepositoryPayload = (repositoryUrl: string) => ({
178 git_http_url: `${repositoryUrl}.git`,
179});