From 2a554ecb8a4a15b39c8d26f58f476d6061a88e5f Mon Sep 17 00:00:00 2001 From: Sarthak-Nayak Date: Mon, 25 May 2026 14:30:56 +0530 Subject: [PATCH] test: add unit tests for safeCompare timing-safe comparison --- src/app/api/webhooks/github/route.test.ts | 52 +++++++++++++++++++++++ src/app/api/webhooks/github/route.ts | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/app/api/webhooks/github/route.test.ts diff --git a/src/app/api/webhooks/github/route.test.ts b/src/app/api/webhooks/github/route.test.ts new file mode 100644 index 00000000..7dabed81 --- /dev/null +++ b/src/app/api/webhooks/github/route.test.ts @@ -0,0 +1,52 @@ +import crypto from "crypto"; +import { describe, it, expect, beforeEach, afterEach, vi, type MockInstance } from "vitest"; +import { safeCompare } from "./route"; + +describe("safeCompare", () => { + let timingSafeEqualSpy: MockInstance; + + beforeEach(() => { + // Vitest uses vi.spyOn to monitor module methods + timingSafeEqualSpy = vi.spyOn(crypto, "timingSafeEqual"); + }); + + afterEach(() => { + // Clear mock data and restore original implementation + timingSafeEqualSpy.mockRestore(); + }); + + // Test Case 1: Early return optimization on length mismatch + it("should return false immediately and not call timingSafeEqual when lengths differ", () => { + const result = safeCompare("short", "muchlongerstring"); + + expect(result).toBe(false); + expect(timingSafeEqualSpy).not.toHaveBeenCalled(); + }); + + // Test Case 2: Standard identical buffers + it("should return true and call timingSafeEqual when buffers are identical", () => { + const stringA = "secure_token_123"; + const stringB = "secure_token_123"; + + const result = safeCompare(stringA, stringB); + + expect(result).toBe(true); + expect(timingSafeEqualSpy).toHaveBeenCalledTimes(1); + }); + + // Test Case 3: Handles empty strings safely + it("should handle empty strings correctly without throwing exceptions", () => { + const result = safeCompare("", ""); + + expect(result).toBe(true); + expect(timingSafeEqualSpy).toHaveBeenCalledTimes(1); + }); + + // Test Case 4: Identical length but different content (Should trigger full timing check) + it("should return false and call timingSafeEqual when lengths match but content differs", () => { + const result = safeCompare("abc", "xyz"); + + expect(result).toBe(false); + expect(timingSafeEqualSpy).toHaveBeenCalledTimes(1); + }); +}); \ No newline at end of file diff --git a/src/app/api/webhooks/github/route.ts b/src/app/api/webhooks/github/route.ts index 25dea87c..ff49a184 100644 --- a/src/app/api/webhooks/github/route.ts +++ b/src/app/api/webhooks/github/route.ts @@ -27,7 +27,7 @@ function getExpectedSignature(secret: string, body: string): string { return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`; } -function safeCompare(a: string, b: string): boolean { +export function safeCompare(a: string, b: string): boolean { const left = Buffer.from(a, "utf8"); const right = Buffer.from(b, "utf8");