From 216b6f6f4e377fe23513976c6ad65a3c373fc759 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Fri, 26 Jun 2026 19:04:02 +0000 Subject: [PATCH 1/4] test: add unit tests for crypto-edge.ts --- test/crypto-edge.test.ts | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 test/crypto-edge.test.ts diff --git a/test/crypto-edge.test.ts b/test/crypto-edge.test.ts new file mode 100644 index 000000000..af0e88486 --- /dev/null +++ b/test/crypto-edge.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, beforeEach } from "vitest"; +const TEST_KEY = "a".repeat(64); +describe("decryptTokenEdge", () => { + beforeEach(() => { process.env.ENCRYPTION_KEY = TEST_KEY; }); + it("returns null when ENCRYPTION_KEY is missing", async () => { + delete process.env.ENCRYPTION_KEY; + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("deadbeef", "a".repeat(24))).toBeNull(); + }); + it("returns null when ENCRYPTION_KEY is invalid format", async () => { + process.env.ENCRYPTION_KEY = "short"; + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("deadbeef", "a".repeat(24))).toBeNull(); + }); + it("returns null for non-hex encrypted string", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("not-hex!", "a".repeat(24))).toBeNull(); + }); + it("returns null for odd-length encrypted string", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("deadbeef", "a".repeat(24))).toBeNull(); + }); + it("returns null for non-hex iv", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("a".repeat(64), "not-hex!")).toBeNull(); + }); + it("returns null when iv is not 24 chars", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("a".repeat(64), "a".repeat(20))).toBeNull(); + }); + it("returns null for tampered ciphertext", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + const { encryptToken } = await import("../src/lib/crypto"); + const { encrypted, iv } = encryptToken("hello world"); + expect(await decryptTokenEdge(encrypted.slice(0,-2)+"ff", iv)).toBeNull(); + }); + it("returns null for hex with invalid characters", async () => { + const { decryptTokenEdge } = await import("../src/lib/crypto-edge"); + expect(await decryptTokenEdge("g".repeat(64), "a".repeat(24))).toBeNull(); + }); +}); From 1ffd83536829f49ad6f3eddd2a0ef334f7ad1b3e Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Fri, 26 Jun 2026 19:04:25 +0000 Subject: [PATCH 2/4] test: add unit tests for upstash-rest.ts --- test/upstash-rest.test.ts | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/upstash-rest.test.ts diff --git a/test/upstash-rest.test.ts b/test/upstash-rest.test.ts new file mode 100644 index 000000000..4074fd5f3 --- /dev/null +++ b/test/upstash-rest.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { getUpstashConfig, upstashPipeline, upstashRateLimitFixedWindow, upstashTryAcquireLock } from "../src/lib/upstash-rest"; +describe("getUpstashConfig", () => { + beforeEach(() => { delete process.env.UPSTASH_REDIS_REST_URL; delete process.env.UPSTASH_REDIS_REST_TOKEN; }); + it("returns null when neither env var is set", () => { expect(getUpstashConfig()).toBeNull(); }); + it("returns null when only URL is set", () => { process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; expect(getUpstashConfig()).toBeNull(); }); + it("returns null when only token is set", () => { process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; expect(getUpstashConfig()).toBeNull(); }); + it("returns config when both env vars are set", () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; + process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + expect(getUpstashConfig()).toEqual({ url: "https://test.upstash.io", token: "token123" }); + }); +}); +describe("upstashPipeline", () => { + beforeEach(() => { delete process.env.UPSTASH_REDIS_REST_URL; delete process.env.UPSTASH_REDIS_REST_TOKEN; vi.restoreAllMocks(); }); + it("returns empty array when not configured", async () => { expect(await upstashPipeline([["GET","key"]])).toEqual([]); }); + it("returns empty array when fetch fails", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(null, { status: 500 })) as unknown as typeof fetch; + expect(await upstashPipeline([["GET","key"]])).toEqual([]); + }); + it("returns parsed JSON on success", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify([{result:"OK"}]), {status:200,headers:{"content-type":"application/json"}})) as unknown as typeof fetch; + expect(await upstashPipeline([["SET","key","value"]])).toEqual([{result:"OK"}]); + }); +}); +describe("upstashRateLimitFixedWindow", () => { + beforeEach(() => { delete process.env.UPSTASH_REDIS_REST_URL; delete process.env.UPSTASH_REDIS_REST_TOKEN; vi.restoreAllMocks(); }); + it("returns allowed when not configured", async () => { expect(await upstashRateLimitFixedWindow({key:"test",limit:5,windowSeconds:60})).toEqual({allowed:true}); }); + it("returns allowed when upstash fetch fails", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(null,{status:500})) as unknown as typeof fetch; + expect(await upstashRateLimitFixedWindow({key:"test",limit:5,windowSeconds:60})).toEqual({allowed:true}); + }); + it("returns allowed when count is NaN", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify([{result:NaN},{result:60}]),{status:200,headers:{"content-type":"application/json"}})) as unknown as typeof fetch; + expect(await upstashRateLimitFixedWindow({key:"test",limit:5,windowSeconds:60})).toEqual({allowed:true}); + }); + it("returns not allowed when count exceeds limit", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify([{result:10},{result:45}]),{status:200,headers:{"content-type":"application/json"}})) as unknown as typeof fetch; + expect(await upstashRateLimitFixedWindow({key:"test",limit:5,windowSeconds:60})).toEqual({allowed:false,retryAfter:45}); + }); +}); +describe("upstashTryAcquireLock", () => { + beforeEach(() => { delete process.env.UPSTASH_REDIS_REST_URL; delete process.env.UPSTASH_REDIS_REST_TOKEN; vi.restoreAllMocks(); }); + it("returns false when not configured", async () => { expect(await upstashTryAcquireLock({key:"lock:test",ttlSeconds:10})).toBe(false); }); + it("returns true when SET returns OK", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify([{result:"OK"}]),{status:200,headers:{"content-type":"application/json"}})) as unknown as typeof fetch; + expect(await upstashTryAcquireLock({key:"lock:test",ttlSeconds:10})).toBe(true); + }); + it("returns false when SET returns null", async () => { + process.env.UPSTASH_REDIS_REST_URL = "https://test.upstash.io"; process.env.UPSTASH_REDIS_REST_TOKEN = "token123"; + globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify([{result:null}]),{status:200,headers:{"content-type":"application/json"}})) as unknown as typeof fetch; + expect(await upstashTryAcquireLock({key:"lock:test",ttlSeconds:10})).toBe(false); + }); +}); From d1ac01668cedebccb44317da08ae9672ac7f19c9 Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Fri, 26 Jun 2026 19:04:40 +0000 Subject: [PATCH 3/4] test: add unit tests for goals/share.ts --- test/goals-share.test.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/goals-share.test.ts diff --git a/test/goals-share.test.ts b/test/goals-share.test.ts new file mode 100644 index 000000000..2fc56f162 --- /dev/null +++ b/test/goals-share.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from "vitest"; +import { getGoalProgressPercent, buildPublicGoalSharePath, buildPublicGoalShareUrl } from "../src/lib/goals/share"; +describe("getGoalProgressPercent", () => { + it("returns 0 for zero target", () => { expect(getGoalProgressPercent(5, 0)).toBe(0); }); + it("returns 0 for negative target", () => { expect(getGoalProgressPercent(5, -10)).toBe(0); }); + it("returns 0 when current is negative", () => { expect(getGoalProgressPercent(-5, 10)).toBe(0); }); + it("returns 0 when current is NaN", () => { expect(getGoalProgressPercent(NaN, 10)).toBe(0); }); + it("returns 0 when target is NaN", () => { expect(getGoalProgressPercent(5, NaN)).toBe(0); }); + it("returns 0 when target is Infinity", () => { expect(getGoalProgressPercent(5, Infinity)).toBe(0); }); + it("returns 0 when current is Infinity", () => { expect(getGoalProgressPercent(Infinity, 10)).toBe(0); }); + it("returns correct percentage for simple values", () => { + expect(getGoalProgressPercent(5, 10)).toBe(50); + expect(getGoalProgressPercent(1, 3)).toBe(33); + expect(getGoalProgressPercent(3, 4)).toBe(75); + }); + it("rounds to nearest integer", () => { + expect(getGoalProgressPercent(1, 6)).toBe(17); + expect(getGoalProgressPercent(2, 3)).toBe(67); + }); + it("caps at 100", () => { + expect(getGoalProgressPercent(15, 10)).toBe(100); + expect(getGoalProgressPercent(1000, 10)).toBe(100); + }); + it("floors at 0", () => { expect(getGoalProgressPercent(-50, 10)).toBe(0); }); +}); +describe("buildPublicGoalSharePath", () => { + it("builds correct path for normal inputs", () => { expect(buildPublicGoalSharePath("alice", "goal-123")).toBe("/u/alice/goals/goal-123"); }); + it("encodes username with special characters", () => { + expect(buildPublicGoalSharePath("alice bob", "goal-123")).toBe("/u/alice%20bob/goals/goal-123"); + expect(buildPublicGoalSharePath("alice@example", "goal-123")).toBe("/u/alice%40example/goals/goal-123"); + }); + it("encodes goalId with special characters", () => { + expect(buildPublicGoalSharePath("alice", "goal/123")).toBe("/u/alice/goals/goal%2F123"); + expect(buildPublicGoalSharePath("alice", "goal#123")).toBe("/u/alice/goals/goal%23123"); + }); +}); +describe("buildPublicGoalShareUrl", () => { + it("builds correct URL", () => { expect(buildPublicGoalShareUrl("https://app.devtrack.io", "alice", "goal-123")).toBe("https://app.devtrack.io/u/alice/goals/goal-123"); }); + it("encodes special characters in path", () => { expect(buildPublicGoalShareUrl("https://app.devtrack.io", "alice bob", "goal-123")).toBe("https://app.devtrack.io/u/alice%20bob/goals/goal-123"); }); +}); From 2053cca933a96772b8d5eeb67d53988e4c5a60bd Mon Sep 17 00:00:00 2001 From: tmdeveloper007 Date: Fri, 26 Jun 2026 19:04:54 +0000 Subject: [PATCH 4/4] test: add unit tests for crypto.ts --- test/crypto.test.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 test/crypto.test.ts diff --git a/test/crypto.test.ts b/test/crypto.test.ts new file mode 100644 index 000000000..aeceb1848 --- /dev/null +++ b/test/crypto.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { encryptToken, decryptToken, safeCompare, getExpectedSignature, verifyGitHubSignature } from "../src/lib/crypto"; +const TEST_KEY = "a".repeat(64); +describe("encryptToken / decryptToken", () => { + beforeEach(() => { process.env.ENCRYPTION_KEY = TEST_KEY; }); + it("roundtrips a simple string", () => { + const { encrypted, iv } = encryptToken("hello world"); + expect(typeof encrypted).toBe("string"); expect(typeof iv).toBe("string"); expect(iv.length).toBe(24); + expect(decryptToken(encrypted, iv)).toBe("hello world"); + }); + it("roundtrips unicode string", () => { + const { encrypted, iv } = encryptToken("hello, world! with emoji"); + expect(decryptToken(encrypted, iv)).toBe("hello, world! with emoji"); + }); + it("returns null for wrong iv length", () => { const { encrypted } = encryptToken("hello"); expect(decryptToken(encrypted, "a".repeat(20))).toBeNull(); }); + it("returns null for tampered ciphertext", () => { const { encrypted, iv } = encryptToken("hello"); expect(decryptToken(encrypted.slice(0,-2)+"ff", iv)).toBeNull(); }); + it("throws when ENCRYPTION_KEY is not set", () => { delete process.env.ENCRYPTION_KEY; expect(() => encryptToken("hello")).toThrow(); }); + it("throws when ENCRYPTION_KEY is invalid format", () => { process.env.ENCRYPTION_KEY = "short"; expect(() => encryptToken("hello")).toThrow(); }); + it("produces different ciphertexts for same plaintext (random IV)", () => { + const { encrypted: e1, iv: iv1 } = encryptToken("hello"); + const { encrypted: e2, iv: iv2 } = encryptToken("hello"); + expect(e1).not.toBe(e2); expect(iv1).not.toBe(iv2); + }); +}); +describe("safeCompare", () => { + it("returns true for equal strings", () => { expect(safeCompare("hello","hello")).toBe(true); }); + it("returns false for different strings", () => { expect(safeCompare("hello","world")).toBe(false); }); + it("returns false for strings of different length", () => { expect(safeCompare("hello","hi")).toBe(false); }); + it("handles empty strings", () => { expect(safeCompare("","")).toBe(true); expect(safeCompare("","a")).toBe(false); }); + it("returns boolean", () => { expect(typeof safeCompare("abc","abc")).toBe("boolean"); }); +}); +describe("getExpectedSignature", () => { + it("returns sha256=... format", () => { expect(getExpectedSignature("secret","body").startsWith("sha256=")).toBe(true); }); + it("produces consistent output for same inputs", () => { expect(getExpectedSignature("secret","body")).toBe(getExpectedSignature("secret","body")); }); + it("produces different output for different secrets", () => { expect(getExpectedSignature("secret1","body")).not.toBe(getExpectedSignature("secret2","body")); }); + it("produces different output for different bodies", () => { expect(getExpectedSignature("secret","body1")).not.toBe(getExpectedSignature("secret","body2")); }); +}); +describe("verifyGitHubSignature", () => { + it("returns true for valid signature", () => { + const body = '{"action":"opened"}'; const secret = "mywebhooksecret"; + expect(verifyGitHubSignature(body, getExpectedSignature(secret,body), secret)).toBe(true); + }); + it("returns false for wrong secret", () => { + const body = '{"action":"opened"}'; + expect(verifyGitHubSignature(body, getExpectedSignature("secret1",body), "secret2")).toBe(false); + }); + it("returns false for tampered body", () => { expect(verifyGitHubSignature("tampered", getExpectedSignature("secret","original"), "secret")).toBe(false); }); + it("returns false for null signature", () => { expect(verifyGitHubSignature("body", null, "secret")).toBe(false); }); + it("returns false for signature without sha256= prefix", () => { + const sig = getExpectedSignature("secret","body").replace("sha256=",""); + expect(verifyGitHubSignature("body", sig, "secret")).toBe(false); + }); +});