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
163 changes: 163 additions & 0 deletions src/__tests__/api/doubts-check-duplicate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
process.env.GROQ_API_KEY = "mock-groq-key";

import { currentUser } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
import { requireMembership } from "@/lib/auth/membership-guard";

jest.mock("@/lib/ai/groq-client", () => ({
groq: {
embeddings: {
create: jest.fn(),
},
chat: {
completions: {
create: jest.fn(),
},
},
},
}));

import { POST } from "@/app/api/doubts/check-duplicate/route";
import { db } from "@/configs/db";
import { enforceApiRateLimit } from "@/lib/ratelimit/api-rate-limit";
import { getAnonymousQuotaIdentifier } from "@/lib/auth/request-identity";
import { getSafeErrorDetails } from "@/lib/errors/safe-error-details";

jest.mock("@clerk/nextjs/server", () => ({
currentUser: jest.fn(),
}));

jest.mock("@/lib/auth/membership-guard", () => {
const actual = jest.requireActual("@/lib/auth/membership-guard");
return {
...actual,
requireMembership: jest.fn(),
};
});

jest.mock("@/lib/ratelimit/api-rate-limit", () => ({
enforceApiRateLimit: jest.fn(),
}));

jest.mock("@/lib/ai/kill-switch", () => ({
buildAiProviderErrorResponse: jest.fn(
() =>
new Response(JSON.stringify({ error: "AI provider unavailable" }), {
status: 503,
}),
),
enforceAiAvailability: jest.fn().mockResolvedValue(null),
}));

const createQueryMock = () => {
const query: any = {
from: () => query,
where: () => query,
orderBy: () => query,
limit: () => query,
then: (resolve: any) => Promise.resolve(resolve([])),
};
return query;
};

jest.mock("@/configs/db", () => ({
db: {
select: jest.fn(() => createQueryMock()),
},
}));



describe("Doubt check-duplicate API endpoint", () => {
const currentUserMock = currentUser as jest.MockedFunction<typeof currentUser>;
const enforceApiRateLimitMock = enforceApiRateLimit as jest.MockedFunction<
typeof enforceApiRateLimit
>;
const dbSelectMock = db.select as jest.Mock;

beforeEach(() => {
currentUserMock.mockReset();
currentUserMock.mockResolvedValue(null);
enforceApiRateLimitMock.mockReset();
enforceApiRateLimitMock.mockResolvedValue(null);
dbSelectMock.mockClear();
});

it("allows anonymous community duplicate checks", async () => {
const req = new Request("http://localhost/api/doubts/check-duplicate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: "How does photosynthesis convert light into energy?",
}),
});

const res = await POST(req);

expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ similarDoubts: [] });
expect(currentUserMock).not.toHaveBeenCalled();
});

it("stops rate-limited requests before querying database", async () => {
enforceApiRateLimitMock.mockResolvedValue(
NextResponse.json({ error: "Too many requests" }, { status: 429 }),
);
const req = new Request("http://localhost/api/doubts/check-duplicate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: "How does photosynthesis convert light into energy?",
}),
});

const res = await POST(req);

expect(res.status).toBe(429);
expect(dbSelectMock).not.toHaveBeenCalled();
});

it("requires authentication for classroom duplicate checks", async () => {
const req = new Request("http://localhost/api/doubts/check-duplicate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: "How does photosynthesis convert light into energy?",
classroomId: 7,
}),
});

const res = await POST(req);

expect(res.status).toBe(401);
await expect(res.json()).resolves.toMatchObject({ error: "Unauthorized" });
});

it("succeeds for authenticated classroom duplicate checks", async () => {
const requireMembershipMock = requireMembership as jest.MockedFunction<typeof requireMembership>;
requireMembershipMock.mockResolvedValue({ role: "student" });
currentUserMock.mockResolvedValue({
primaryEmailAddress: { emailAddress: "student@example.com" }
} as any);

const req = new Request("http://localhost/api/doubts/check-duplicate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: "How does photosynthesis convert light into energy?",
classroomId: 7,
}),
});

const res = await POST(req);

expect(res.status).toBe(200);
await expect(res.json()).resolves.toEqual({ similarDoubts: [] });
expect(enforceApiRateLimitMock).toHaveBeenCalledWith(
expect.anything(),
"student@example.com",
"ai"
);
expect(requireMembershipMock).toHaveBeenCalledWith("student@example.com", 7);
});
});
59 changes: 59 additions & 0 deletions src/__tests__/lib/embeddings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
process.env.GROQ_API_KEY = "mock-groq-key";

import { safeGenerateEmbedding, findSemanticDuplicates } from "@/lib/ai/embeddings";
import { groq } from "@/lib/ai/groq-client";
import { db } from "@/configs/db";

jest.mock("@/lib/ai/groq-client", () => ({
groq: {
embeddings: {
create: jest.fn(),
},
},
}));

jest.mock("@/configs/db", () => ({
db: {
select: jest.fn(() => ({
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
limit: jest.fn().mockResolvedValue([]),
})),
},
}));

describe("Embeddings & Vector Duplicate Utilities", () => {
beforeEach(() => {
jest.clearAllMocks();
});

it("safeGenerateEmbedding returns vector array when Groq embeddings succeeds", async () => {
const mockVector = new Array(1536).fill(0.1);
(groq.embeddings.create as jest.Mock).mockResolvedValueOnce({
data: [{ embedding: mockVector }],
});

const result = await safeGenerateEmbedding("What is calculus?");
expect(result).toEqual(mockVector);
expect(groq.embeddings.create).toHaveBeenCalledWith({
model: "nomic-embed-text",
input: "What is calculus?",
encoding_format: "float",
});
});

it("safeGenerateEmbedding returns null safely on error", async () => {
(groq.embeddings.create as jest.Mock).mockRejectedValueOnce(new Error("API rate limit"));

const result = await safeGenerateEmbedding("What is calculus?");
expect(result).toBeNull();
});

it("findSemanticDuplicates returns empty list when no embedding generated", async () => {
(groq.embeddings.create as jest.Mock).mockRejectedValueOnce(new Error("Network failure"));

const duplicates = await findSemanticDuplicates({ content: "What is momentum?" });
expect(duplicates).toEqual([]);
});
});
Loading
Loading