Skip to content

Commit db38318

Browse files
committed
test: strengthen Garmin webhook coverage
1 parent 8f68469 commit db38318

1 file changed

Lines changed: 73 additions & 3 deletions

File tree

convex/garmin/webhookSignature.test.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,53 @@ import { garminWebhookFailureStatus, verifyGarminWebhookSignature } from "./webh
55
class TrackingRequest extends Request {
66
bodyWasRead = false;
77

8-
override async text(): Promise<string> {
8+
private trackBodyRead(): void {
99
this.bodyWasRead = true;
10+
}
11+
12+
override get body() {
13+
this.trackBodyRead();
14+
return super.body;
15+
}
16+
17+
override async arrayBuffer(): Promise<ArrayBuffer> {
18+
this.trackBodyRead();
19+
return await super.arrayBuffer();
20+
}
21+
22+
override async blob(): Promise<Blob> {
23+
this.trackBodyRead();
24+
return await super.blob();
25+
}
26+
27+
override clone(): Request {
28+
this.trackBodyRead();
29+
return super.clone();
30+
}
31+
32+
override async formData(): Promise<FormData> {
33+
this.trackBodyRead();
34+
return await super.formData();
35+
}
36+
37+
override async json(): Promise<unknown> {
38+
this.trackBodyRead();
39+
return await super.json();
40+
}
41+
42+
override async text(): Promise<string> {
43+
this.trackBodyRead();
1044
return await super.text();
1145
}
1246
}
1347

14-
async function runGarminRoute(req: Request): Promise<Response> {
48+
async function runGarminRoute(req: Request, ctx: object = {}): Promise<Response> {
1549
const route = http.lookup(new URL(req.url).pathname, "POST");
1650
if (!route) throw new Error("Expected Garmin path-secret route");
1751
const handler = route[0] as unknown as {
1852
_handler: (ctx: object, request: Request) => Promise<Response>;
1953
};
20-
return await handler._handler({}, req);
54+
return await handler._handler(ctx, req);
2155
}
2256

2357
describe("Garmin path-secret webhook routes", () => {
@@ -31,6 +65,35 @@ describe("Garmin path-secret webhook routes", () => {
3165
expect(pathSecretRoute).not.toBeNull();
3266
});
3367

68+
it("processes an authenticated path-secret delivery through the registered handler", async () => {
69+
vi.stubEnv("GARMIN_WEBHOOK_SECRET", "secret-1");
70+
const store = vi.fn().mockResolvedValue("storage-1");
71+
const runMutation = vi.fn().mockResolvedValue("event-1");
72+
const runAfter = vi.fn().mockResolvedValue(undefined);
73+
const req = new Request("https://example.com/garmin/webhook/activities/secret-1", {
74+
method: "POST",
75+
body: '{"activities":[]}',
76+
});
77+
78+
const response = await runGarminRoute(req, {
79+
storage: { store },
80+
runMutation,
81+
scheduler: { runAfter },
82+
});
83+
84+
expect(response.status).toBe(200);
85+
expect(store).toHaveBeenCalledOnce();
86+
expect(runMutation).toHaveBeenCalledWith(expect.anything(), {
87+
eventType: "activities",
88+
rawPayloadStorageId: "storage-1",
89+
});
90+
expect(runAfter).toHaveBeenCalledWith(0, expect.anything(), {
91+
eventId: "event-1",
92+
eventType: "activities",
93+
rawPayloadStorageId: "storage-1",
94+
});
95+
});
96+
3497
it("does not route path-secret requests for unsupported event types", () => {
3598
const req = new TrackingRequest("https://example.com/garmin/webhook/notSupported/secret-1", {
3699
method: "POST",
@@ -89,6 +152,13 @@ describe("verifyGarminWebhookSignature", () => {
89152
await expect(verifyGarminWebhookSignature(req, "{}")).resolves.toEqual({ valid: true });
90153
});
91154

155+
it("decodes the path secret before comparing it", async () => {
156+
vi.stubEnv("GARMIN_WEBHOOK_SECRET", "secret/1");
157+
const req = new Request("https://example.com/garmin/webhook/activities/secret%2F1");
158+
159+
await expect(verifyGarminWebhookSignature(req, "{}")).resolves.toEqual({ valid: true });
160+
});
161+
92162
it("rejects a valid secret when the path contains an extra segment", async () => {
93163
vi.stubEnv("GARMIN_WEBHOOK_SECRET", "secret-1");
94164
const req = new Request("https://example.com/garmin/webhook/activities/extra/secret-1");

0 commit comments

Comments
 (0)