mirror of
https://github.com/willchen96/mike.git
synced 2026-07-24 23:41:04 +02:00
Merge pull request #228 from amal66/olp-pr/test-harness
[Testing 05] test: minimal vitest harness for backend and frontend
This commit is contained in:
commit
8a34a6457f
7 changed files with 2683 additions and 31 deletions
1230
backend/package-lock.json
generated
1230
backend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
"start": "node dist/index.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.90.0",
|
||||
|
|
@ -38,7 +39,8 @@
|
|||
"@types/node": "^22.14.1",
|
||||
"prettier": "^3.8.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.3"
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
|
|
|
|||
109
backend/src/lib/__tests__/downloadTokens.test.ts
Normal file
109
backend/src/lib/__tests__/downloadTokens.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { signDownload, verifyDownload, buildDownloadUrl } from "../downloadTokens";
|
||||
|
||||
const SECRET = "test-secret-32-bytes-long-enough!!";
|
||||
|
||||
beforeAll(() => {
|
||||
process.env.DOWNLOAD_SIGNING_SECRET = SECRET;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete process.env.DOWNLOAD_SIGNING_SECRET;
|
||||
});
|
||||
|
||||
describe("signDownload", () => {
|
||||
it("returns a two-part dot-separated token", () => {
|
||||
const token = signDownload("documents/user/doc.pdf", "contract.pdf");
|
||||
const parts = token.split(".");
|
||||
expect(parts).toHaveLength(2);
|
||||
expect(parts[0].length).toBeGreaterThan(0);
|
||||
expect(parts[1].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("produces different tokens for different paths", () => {
|
||||
const t1 = signDownload("documents/a/file.pdf", "a.pdf");
|
||||
const t2 = signDownload("documents/b/file.pdf", "b.pdf");
|
||||
expect(t1).not.toBe(t2);
|
||||
});
|
||||
|
||||
it("uses base64url characters only (no +, /, =)", () => {
|
||||
const token = signDownload("documents/user/file.pdf", "file.pdf");
|
||||
expect(token).not.toMatch(/[+/=]/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("verifyDownload", () => {
|
||||
it("round-trips a valid token", () => {
|
||||
const path = "documents/user123/doc456/source.pdf";
|
||||
const filename = "Contract Final v2.pdf";
|
||||
const token = signDownload(path, filename);
|
||||
const result = verifyDownload(token);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.path).toBe(path);
|
||||
expect(result!.filename).toBe(filename);
|
||||
});
|
||||
|
||||
it("returns null for a tampered payload", () => {
|
||||
const token = signDownload("documents/user/file.pdf", "file.pdf");
|
||||
const [, sig] = token.split(".");
|
||||
const fakePayload = Buffer.from(
|
||||
JSON.stringify({ p: "documents/attacker/file.pdf", f: "file.pdf" }),
|
||||
)
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
expect(verifyDownload(`${fakePayload}.${sig}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a tampered signature", () => {
|
||||
const token = signDownload("documents/user/file.pdf", "file.pdf");
|
||||
const [enc] = token.split(".");
|
||||
const fakeSig = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
expect(verifyDownload(`${enc}.${fakeSig}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a token with too many parts", () => {
|
||||
expect(verifyDownload("a.b.c")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a token with too few parts", () => {
|
||||
expect(verifyDownload("onlyonepart")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when payload JSON is missing required fields", () => {
|
||||
const bad = Buffer.from(JSON.stringify({ x: 1 }))
|
||||
.toString("base64")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/g, "");
|
||||
const sig = Buffer.alloc(32).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
expect(verifyDownload(`${bad}.${sig}`)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when signed with a different secret", () => {
|
||||
const token = signDownload("documents/user/file.pdf", "file.pdf");
|
||||
process.env.DOWNLOAD_SIGNING_SECRET = "different-secret-value-!!";
|
||||
const result = verifyDownload(token);
|
||||
process.env.DOWNLOAD_SIGNING_SECRET = SECRET;
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDownloadUrl", () => {
|
||||
it("returns a path starting with /download/", () => {
|
||||
const url = buildDownloadUrl("documents/user/file.pdf", "file.pdf");
|
||||
expect(url).toMatch(/^\/download\//);
|
||||
});
|
||||
|
||||
it("embeds a verifiable token in the URL", () => {
|
||||
const path = "documents/user/file.pdf";
|
||||
const filename = "file.pdf";
|
||||
const url = buildDownloadUrl(path, filename);
|
||||
const token = url.replace("/download/", "");
|
||||
const result = verifyDownload(token);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.path).toBe(path);
|
||||
expect(result!.filename).toBe(filename);
|
||||
});
|
||||
});
|
||||
|
|
@ -16,5 +16,5 @@
|
|||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/__tests__/**"]
|
||||
}
|
||||
|
|
|
|||
1320
frontend/package-lock.json
generated
1320
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,6 +7,7 @@
|
|||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"test": "vitest run",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
|
|
@ -73,6 +74,7 @@
|
|||
"tailwindcss": "^4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5",
|
||||
"vitest": "^4.1.9",
|
||||
"wrangler": "^4.90.0"
|
||||
},
|
||||
"license": "AGPL-3.0-only"
|
||||
|
|
|
|||
45
frontend/src/app/lib/utils.test.ts
Normal file
45
frontend/src/app/lib/utils.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { cn, diceCoefficient, isFuzzyMatch } from "./utils";
|
||||
|
||||
describe("cn", () => {
|
||||
it("joins truthy class names and drops falsy ones", () => {
|
||||
expect(cn("a", false && "b", undefined, "c")).toBe("a c");
|
||||
});
|
||||
|
||||
it("merges conflicting tailwind classes, keeping the last", () => {
|
||||
expect(cn("px-2", "px-4")).toBe("px-4");
|
||||
});
|
||||
});
|
||||
|
||||
describe("diceCoefficient", () => {
|
||||
it("returns 1 for identical strings (ignoring case and punctuation)", () => {
|
||||
expect(diceCoefficient("Hello, World!", "hello world")).toBe(1);
|
||||
});
|
||||
|
||||
it("returns 0 when either input is empty", () => {
|
||||
expect(diceCoefficient("", "anything")).toBe(0);
|
||||
expect(diceCoefficient("anything", "")).toBe(0);
|
||||
});
|
||||
|
||||
it("returns a partial score for partially overlapping strings", () => {
|
||||
const score = diceCoefficient("night", "nacht");
|
||||
expect(score).toBeGreaterThan(0);
|
||||
expect(score).toBeLessThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFuzzyMatch", () => {
|
||||
it("matches near-identical strings above the default threshold", () => {
|
||||
expect(isFuzzyMatch("organization", "organisation")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects clearly different strings", () => {
|
||||
expect(isFuzzyMatch("apple", "zebra")).toBe(false);
|
||||
});
|
||||
|
||||
it("respects a custom threshold", () => {
|
||||
// "night" vs "nacht" scores ~0.25 — passes a low bar, fails a high one.
|
||||
expect(isFuzzyMatch("night", "nacht", 0.1)).toBe(true);
|
||||
expect(isFuzzyMatch("night", "nacht", 0.9)).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue