Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npm test *)"
]
}
}
99 changes: 99 additions & 0 deletions app/api/routes-f/__tests__/char-stats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { POST } from "../char-stats/route";

function makeReq(body: unknown) {
return new NextRequest("http://localhost/api/routes-f/char-stats", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}

describe("/api/routes-f/char-stats", () => {
it("counts ASCII text correctly", async () => {
const res = await POST(makeReq({ text: "Hello, World! 123" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.total).toBe(17);
expect(data.letters).toBe(10);
expect(data.digits).toBe(3);
expect(data.punctuation).toBeGreaterThanOrEqual(1);
expect(data.whitespace).toBe(2);
expect(data.emoji).toBe(0);
expect(data.by_script.latin).toBe(10);
});

it("counts mixed scripts", async () => {
const res = await POST(makeReq({ text: "Hello Привет" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.by_script.latin).toBe(5);
expect(data.by_script.cyrillic).toBe(6);
expect(data.letters).toBe(11);
expect(data.whitespace).toBe(1);
});

it("counts CJK characters", async () => {
const res = await POST(makeReq({ text: "你好世界" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.letters).toBe(4);
expect(data.by_script.cjk).toBe(4);
});

it("counts emoji", async () => {
const res = await POST(makeReq({ text: "Hi 👋🏽 there 😊" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.emoji).toBeGreaterThanOrEqual(2);
});

it("handles ZWJ emoji sequences", async () => {
// Family emoji: man+woman+girl+boy via ZWJ
const family = "👨‍👩‍👧‍👦";
const res = await POST(makeReq({ text: family }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.emoji).toBe(1);
});

it("handles empty string", async () => {
const res = await POST(makeReq({ text: "" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.total).toBe(0);
expect(data.letters).toBe(0);
expect(data.emoji).toBe(0);
});

it("counts digits and symbols", async () => {
const res = await POST(makeReq({ text: "42 + 58 = 100" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.digits).toBe(7);
expect(data.whitespace).toBe(4);
});

it("rejects non-string text", async () => {
const res = await POST(makeReq({ text: 42 }));
expect(res.status).toBe(400);
});

it("rejects missing text field", async () => {
const res = await POST(makeReq({}));
expect(res.status).toBe(400);
});

it("rejects invalid JSON", async () => {
const req = new NextRequest("http://localhost/api/routes-f/char-stats", {
method: "POST",
headers: { "content-type": "application/json" },
body: "bad",
});
const res = await POST(req);
expect(res.status).toBe(400);
});
});
139 changes: 139 additions & 0 deletions app/api/routes-f/__tests__/cookie-parse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { POST } from "../cookie-parse/route";

function makeReq(body: unknown) {
return new NextRequest("http://localhost/api/routes-f/cookie-parse", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}

describe("/api/routes-f/cookie-parse", () => {
describe("parse mode", () => {
it("parses a simple cookie", async () => {
const res = await POST(makeReq({ mode: "parse", input: "session=abc123" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.session.value).toBe("abc123");
expect(data.session.secure).toBe(false);
expect(data.session.http_only).toBe(false);
});

it("parses a cookie with all attributes", async () => {
const input =
"token=xyz; Expires=Wed, 01 Jan 2025 00:00:00 GMT; Max-Age=3600; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Strict";
const res = await POST(makeReq({ mode: "parse", input }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.token.value).toBe("xyz");
expect(data.token.expires).toBe("Wed, 01 Jan 2025 00:00:00 GMT");
expect(data.token.max_age).toBe(3600);
expect(data.token.domain).toBe("example.com");
expect(data.token.path).toBe("/");
expect(data.token.secure).toBe(true);
expect(data.token.http_only).toBe(true);
expect(data.token.same_site).toBe("Strict");
});

it("handles URL-encoded values", async () => {
const res = await POST(makeReq({ mode: "parse", input: "user=hello%20world" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.user.value).toBe("hello world");
});

it("rejects invalid SameSite value", async () => {
const res = await POST(
makeReq({ mode: "parse", input: "x=1; SameSite=Invalid" })
);
expect(res.status).toBe(400);
});
});

describe("build mode", () => {
it("builds a simple Set-Cookie header", async () => {
const res = await POST(
makeReq({ mode: "build", input: { name: "session", value: "abc" } })
);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.header).toBe("session=abc");
});

it("builds a full Set-Cookie header", async () => {
const res = await POST(
makeReq({
mode: "build",
input: {
name: "token",
value: "xyz",
path: "/",
secure: true,
http_only: true,
same_site: "Lax",
},
})
);
expect(res.status).toBe(200);
const data = await res.json();
expect(data.header).toContain("token=xyz");
expect(data.header).toContain("Path=/");
expect(data.header).toContain("Secure");
expect(data.header).toContain("HttpOnly");
expect(data.header).toContain("SameSite=Lax");
});

it("round-trips build -> parse", async () => {
const buildRes = await POST(
makeReq({
mode: "build",
input: {
name: "auth",
value: "tok123",
path: "/app",
secure: true,
http_only: true,
same_site: "None",
},
})
);
expect(buildRes.status).toBe(200);
const { header } = await buildRes.json();

const parseRes = await POST(makeReq({ mode: "parse", input: header }));
expect(parseRes.status).toBe(200);
const data = await parseRes.json();
expect(data.auth.value).toBe("tok123");
expect(data.auth.path).toBe("/app");
expect(data.auth.secure).toBe(true);
expect(data.auth.http_only).toBe(true);
expect(data.auth.same_site).toBe("None");
});

it("rejects invalid same_site in build mode", async () => {
const res = await POST(
makeReq({ mode: "build", input: { name: "x", value: "1", same_site: "Bad" } })
);
expect(res.status).toBe(400);
});
});

it("rejects unknown mode", async () => {
const res = await POST(makeReq({ mode: "delete", input: "x=1" }));
expect(res.status).toBe(400);
});

it("rejects invalid JSON", async () => {
const req = new NextRequest("http://localhost/api/routes-f/cookie-parse", {
method: "POST",
headers: { "content-type": "application/json" },
body: "bad",
});
const res = await POST(req);
expect(res.status).toBe(400);
});
});
109 changes: 109 additions & 0 deletions app/api/routes-f/__tests__/gcd-lcm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* @jest-environment node
*/
import { NextRequest } from "next/server";
import { POST } from "../gcd-lcm/route";

function makeReq(body: unknown) {
return new NextRequest("http://localhost/api/routes-f/gcd-lcm", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}

describe("/api/routes-f/gcd-lcm", () => {
it("computes gcd and lcm of a known pair (12, 18)", async () => {
const res = await POST(makeReq({ numbers: [12, 18] }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(6);
expect(data.lcm).toBe(36);
expect(data.n_count).toBe(2);
});

it("computes gcd only when operation=gcd", async () => {
const res = await POST(makeReq({ numbers: [12, 18], operation: "gcd" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(6);
expect(data.lcm).toBeUndefined();
});

it("computes lcm only when operation=lcm", async () => {
const res = await POST(makeReq({ numbers: [12, 18], operation: "lcm" }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.lcm).toBe(36);
expect(data.gcd).toBeUndefined();
});

it("handles multiple numbers", async () => {
const res = await POST(makeReq({ numbers: [4, 6, 8] }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(2);
expect(data.lcm).toBe(24);
expect(data.n_count).toBe(3);
});

it("handles edge case with 1", async () => {
const res = await POST(makeReq({ numbers: [1, 7] }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(1);
expect(data.lcm).toBe(7);
});

it("handles prime numbers", async () => {
const res = await POST(makeReq({ numbers: [7, 11] }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(1);
expect(data.lcm).toBe(77);
});

it("handles single number", async () => {
const res = await POST(makeReq({ numbers: [15] }));
expect(res.status).toBe(200);
const data = await res.json();
expect(data.gcd).toBe(15);
expect(data.lcm).toBe(15);
});

it("rejects non-positive integers", async () => {
const res = await POST(makeReq({ numbers: [0, 5] }));
expect(res.status).toBe(400);
});

it("rejects floats", async () => {
const res = await POST(makeReq({ numbers: [1.5, 3] }));
expect(res.status).toBe(400);
});

it("rejects arrays exceeding 100 numbers", async () => {
const numbers = Array.from({ length: 101 }, (_, i) => i + 1);
const res = await POST(makeReq({ numbers }));
expect(res.status).toBe(400);
});

it("rejects empty array", async () => {
const res = await POST(makeReq({ numbers: [] }));
expect(res.status).toBe(400);
});

it("rejects invalid operation", async () => {
const res = await POST(makeReq({ numbers: [4, 6], operation: "max" }));
expect(res.status).toBe(400);
});

it("rejects invalid JSON", async () => {
const req = new NextRequest("http://localhost/api/routes-f/gcd-lcm", {
method: "POST",
headers: { "content-type": "application/json" },
body: "not-json",
});
const res = await POST(req);
expect(res.status).toBe(400);
});
});
Loading