mirror of
https://github.com/f/awesome-chatgpt-prompts.git
synced 2026-02-12 15:52:47 +00:00
ci: add CI workflow and tests for admin categories and prompts
This commit is contained in:
35
.github/workflows/ci.yml
vendored
Normal file
35
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
DATABASE_URL: "postgresql://test:test@localhost:5432/test"
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
168
src/__tests__/api/admin-categories.test.ts
Normal file
168
src/__tests__/api/admin-categories.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/app/api/admin/categories/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
category: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/cache", () => ({
|
||||
revalidateTag: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/admin/categories", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test", slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 401 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test", slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 400 if name is missing", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Name and slug are required");
|
||||
});
|
||||
|
||||
it("should return 400 if slug is missing", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Name and slug are required");
|
||||
});
|
||||
|
||||
it("should create category with required fields", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.category.create).mockResolvedValue({
|
||||
id: "1",
|
||||
name: "Test Category",
|
||||
slug: "test-category",
|
||||
description: null,
|
||||
icon: null,
|
||||
parentId: null,
|
||||
pinned: false,
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test Category", slug: "test-category" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.name).toBe("Test Category");
|
||||
expect(data.slug).toBe("test-category");
|
||||
});
|
||||
|
||||
it("should create category with optional fields", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.category.create).mockResolvedValue({
|
||||
id: "1",
|
||||
name: "Test Category",
|
||||
slug: "test-category",
|
||||
description: "A test category",
|
||||
icon: "📚",
|
||||
parentId: "parent-1",
|
||||
pinned: true,
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "Test Category",
|
||||
slug: "test-category",
|
||||
description: "A test category",
|
||||
icon: "📚",
|
||||
parentId: "parent-1",
|
||||
pinned: true,
|
||||
}),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.description).toBe("A test category");
|
||||
expect(data.icon).toBe("📚");
|
||||
expect(data.pinned).toBe(true);
|
||||
});
|
||||
|
||||
it("should call db.category.create with correct data", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.category.create).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/categories", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: "My Category",
|
||||
slug: "my-category",
|
||||
description: "Description",
|
||||
icon: "🎯",
|
||||
parentId: "parent-id",
|
||||
pinned: true,
|
||||
}),
|
||||
});
|
||||
await POST(request);
|
||||
|
||||
expect(db.category.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: "My Category",
|
||||
slug: "my-category",
|
||||
description: "Description",
|
||||
icon: "🎯",
|
||||
parentId: "parent-id",
|
||||
pinned: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
207
src/__tests__/api/admin-prompts.test.ts
Normal file
207
src/__tests__/api/admin-prompts.test.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET } from "@/app/api/admin/prompts/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
prompt: {
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/admin/prompts", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should return prompts with pagination for admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "1",
|
||||
title: "Test Prompt",
|
||||
slug: "test-prompt",
|
||||
type: "TEXT",
|
||||
isPrivate: false,
|
||||
isUnlisted: false,
|
||||
isFeatured: false,
|
||||
viewCount: 100,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
author: { id: "user1", username: "user", name: "User", avatar: null },
|
||||
category: null,
|
||||
_count: { votes: 5, reports: 0 },
|
||||
},
|
||||
] as never);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(1);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.prompts).toHaveLength(1);
|
||||
expect(data.pagination.total).toBe(1);
|
||||
});
|
||||
|
||||
it("should apply search filter", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?search=test");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: expect.arrayContaining([
|
||||
expect.objectContaining({ title: expect.any(Object) }),
|
||||
expect.objectContaining({ content: expect.any(Object) }),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should apply filter for unlisted prompts", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?filter=unlisted");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ isUnlisted: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should apply filter for private prompts", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?filter=private");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ isPrivate: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should apply filter for featured prompts", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?filter=featured");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ isFeatured: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination parameters", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(50);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?page=2&limit=10");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
skip: 10,
|
||||
take: 10,
|
||||
})
|
||||
);
|
||||
expect(data.pagination.page).toBe(2);
|
||||
expect(data.pagination.limit).toBe(10);
|
||||
expect(data.pagination.totalPages).toBe(5);
|
||||
});
|
||||
|
||||
it("should limit max items per page to 100", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?limit=500");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
take: 100,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle sorting parameters", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?sortBy=title&sortOrder=asc");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { title: "asc" },
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should default to createdAt desc for invalid sort field", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/prompts?sortBy=invalid");
|
||||
await GET(request);
|
||||
|
||||
expect(db.prompt.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { createdAt: "desc" },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
140
src/__tests__/api/admin-tags.test.ts
Normal file
140
src/__tests__/api/admin-tags.test.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/app/api/admin/tags/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
tag: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/admin/tags", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test", slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 401 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test", slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 400 if name is missing", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ slug: "test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Name and slug are required");
|
||||
});
|
||||
|
||||
it("should return 400 if slug is missing", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Name and slug are required");
|
||||
});
|
||||
|
||||
it("should create tag with required fields", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.tag.create).mockResolvedValue({
|
||||
id: "1",
|
||||
name: "JavaScript",
|
||||
slug: "javascript",
|
||||
color: "#6366f1",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "JavaScript", slug: "javascript" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.name).toBe("JavaScript");
|
||||
expect(data.slug).toBe("javascript");
|
||||
expect(data.color).toBe("#6366f1");
|
||||
});
|
||||
|
||||
it("should create tag with custom color", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.tag.create).mockResolvedValue({
|
||||
id: "1",
|
||||
name: "Python",
|
||||
slug: "python",
|
||||
color: "#3776ab",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Python", slug: "python", color: "#3776ab" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.color).toBe("#3776ab");
|
||||
});
|
||||
|
||||
it("should use default color when not provided", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.tag.create).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/tags", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: "Test", slug: "test" }),
|
||||
});
|
||||
await POST(request);
|
||||
|
||||
expect(db.tag.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
name: "Test",
|
||||
slug: "test",
|
||||
color: "#6366f1",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
192
src/__tests__/api/admin-users.test.ts
Normal file
192
src/__tests__/api/admin-users.test.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET } from "@/app/api/admin/users/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
user: {
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/admin/users", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should return users with pagination for admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "1",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
name: "Test User",
|
||||
avatar: null,
|
||||
role: "USER",
|
||||
verified: true,
|
||||
flagged: false,
|
||||
flaggedAt: null,
|
||||
flaggedReason: null,
|
||||
dailyGenerationLimit: 10,
|
||||
generationCreditsRemaining: 5,
|
||||
createdAt: new Date(),
|
||||
_count: { prompts: 3 },
|
||||
},
|
||||
] as never);
|
||||
vi.mocked(db.user.count).mockResolvedValue(1);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.users).toHaveLength(1);
|
||||
expect(data.pagination.total).toBe(1);
|
||||
});
|
||||
|
||||
it("should apply search filter", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?search=john");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
OR: expect.arrayContaining([
|
||||
expect.objectContaining({ email: expect.any(Object) }),
|
||||
expect.objectContaining({ username: expect.any(Object) }),
|
||||
expect.objectContaining({ name: expect.any(Object) }),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter by admin role", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?filter=admin");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ role: "ADMIN" }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter by verified status", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?filter=verified");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ verified: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter by unverified status", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?filter=unverified");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ verified: false }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should filter by flagged status", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?filter=flagged");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ flagged: true }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle pagination", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(100);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?page=3&limit=25");
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
skip: 50,
|
||||
take: 25,
|
||||
})
|
||||
);
|
||||
expect(data.pagination.page).toBe(3);
|
||||
expect(data.pagination.totalPages).toBe(4);
|
||||
});
|
||||
|
||||
it("should sort by username ascending", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.user.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.user.count).mockResolvedValue(0);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/admin/users?sortBy=username&sortOrder=asc");
|
||||
await GET(request);
|
||||
|
||||
expect(db.user.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
orderBy: { username: "asc" },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
205
src/__tests__/api/comment-flag.test.ts
Normal file
205
src/__tests__/api/comment-flag.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/app/api/prompts/[id]/comments/[commentId]/flag/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { getConfig } from "@/lib/config";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
comment: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/config", () => ({
|
||||
getConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/prompts/[id]/comments/[commentId]/flag", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: true } } as never);
|
||||
});
|
||||
|
||||
it("should return 403 if comments feature is disabled", async () => {
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: false } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("feature_disabled");
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return 404 if comment belongs to different prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "different-prompt",
|
||||
flagged: false,
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should flag an unflagged comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
flagged: false,
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({ flagged: true } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.flagged).toBe(true);
|
||||
});
|
||||
|
||||
it("should unflag a flagged comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
flagged: true,
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({ flagged: false } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.flagged).toBe(false);
|
||||
});
|
||||
|
||||
it("should set flaggedAt and flaggedBy when flagging", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
flagged: false,
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({ flagged: true } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: {
|
||||
flagged: true,
|
||||
flaggedAt: expect.any(Date),
|
||||
flaggedBy: "admin1",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear flaggedAt and flaggedBy when unflagging", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
flagged: true,
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({ flagged: false } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/flag", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: {
|
||||
flagged: false,
|
||||
flaggedAt: null,
|
||||
flaggedBy: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
184
src/__tests__/api/comment-operations.test.ts
Normal file
184
src/__tests__/api/comment-operations.test.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { DELETE } from "@/app/api/prompts/[id]/comments/[commentId]/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { getConfig } from "@/lib/config";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
comment: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/config", () => ({
|
||||
getConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("DELETE /api/prompts/[id]/comments/[commentId]", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: true } } as never);
|
||||
});
|
||||
|
||||
it("should return 403 if comments feature is disabled", async () => {
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: false } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("feature_disabled");
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return 404 if comment belongs to different prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "different-prompt", // Different from params
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not author or admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
authorId: "other-user", // Different author
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should allow author to delete own comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
authorId: "user1", // Same as session user
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.deleted).toBe(true);
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow admin to delete any comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
authorId: "other-user", // Different user, but admin can delete
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.deleted).toBe(true);
|
||||
});
|
||||
|
||||
it("should soft delete by setting deletedAt timestamp", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456", {
|
||||
method: "DELETE",
|
||||
});
|
||||
await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: { deletedAt: expect.any(Date) },
|
||||
});
|
||||
});
|
||||
});
|
||||
400
src/__tests__/api/comment-vote.test.ts
Normal file
400
src/__tests__/api/comment-vote.test.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST, DELETE } from "@/app/api/prompts/[id]/comments/[commentId]/vote/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { getConfig } from "@/lib/config";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
comment: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
commentVote: {
|
||||
findUnique: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/config", () => ({
|
||||
getConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/prompts/[id]/comments/[commentId]/vote", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: true } } as never);
|
||||
});
|
||||
|
||||
it("should return 403 if comments feature is disabled", async () => {
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: false } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("feature_disabled");
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 400 for invalid vote value", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 5 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 400 for missing vote value", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 400 for value of 0", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 0 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return 404 if comment belongs to different prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "different-prompt",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should create new upvote when no existing vote", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
vi.mocked(db.commentVote.findUnique)
|
||||
.mockResolvedValueOnce(null) // No existing vote
|
||||
.mockResolvedValueOnce({ value: 1 } as never); // After create
|
||||
vi.mocked(db.commentVote.create).mockResolvedValue({} as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([{ value: 1 }] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(1);
|
||||
expect(data.userVote).toBe(1);
|
||||
expect(db.commentVote.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userId: "user1",
|
||||
commentId: "456",
|
||||
value: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should create new downvote when no existing vote", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
vi.mocked(db.commentVote.findUnique)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({ value: -1 } as never);
|
||||
vi.mocked(db.commentVote.create).mockResolvedValue({} as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([{ value: -1 }] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: -1 }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(-1);
|
||||
expect(data.userVote).toBe(-1);
|
||||
});
|
||||
|
||||
it("should toggle off when voting same value twice", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
vi.mocked(db.commentVote.findUnique)
|
||||
.mockResolvedValueOnce({ value: 1 } as never) // Existing upvote
|
||||
.mockResolvedValueOnce(null); // After deletion
|
||||
vi.mocked(db.commentVote.delete).mockResolvedValue({} as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }), // Same as existing
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(0);
|
||||
expect(data.userVote).toBe(0);
|
||||
expect(db.commentVote.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should switch vote when voting opposite value", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
vi.mocked(db.commentVote.findUnique)
|
||||
.mockResolvedValueOnce({ value: 1 } as never) // Existing upvote
|
||||
.mockResolvedValueOnce({ value: -1 } as never); // After update
|
||||
vi.mocked(db.commentVote.update).mockResolvedValue({} as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([{ value: -1 }] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: -1 }), // Opposite of existing
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(-1);
|
||||
expect(data.userVote).toBe(-1);
|
||||
expect(db.commentVote.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should update cached score on comment", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.comment.findUnique).mockResolvedValue({
|
||||
id: "456",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
vi.mocked(db.commentVote.findUnique)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce({ value: 1 } as never);
|
||||
vi.mocked(db.commentVote.create).mockResolvedValue({} as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([
|
||||
{ value: 1 },
|
||||
{ value: 1 },
|
||||
{ value: -1 },
|
||||
] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ value: 1 }),
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: { score: 1 }, // 1 + 1 - 1 = 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/prompts/[id]/comments/[commentId]/vote", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: true } } as never);
|
||||
});
|
||||
|
||||
it("should return 403 if comments feature is disabled", async () => {
|
||||
vi.mocked(getConfig).mockResolvedValue({ features: { comments: false } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("feature_disabled");
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should remove vote and return updated score", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.commentVote.deleteMany).mockResolvedValue({ count: 1 } as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([{ value: 1 }] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(1);
|
||||
expect(data.userVote).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle removing non-existent vote gracefully", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.commentVote.deleteMany).mockResolvedValue({ count: 0 } as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.score).toBe(0);
|
||||
expect(data.userVote).toBe(0);
|
||||
});
|
||||
|
||||
it("should update cached score on comment after removing vote", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.commentVote.deleteMany).mockResolvedValue({ count: 1 } as never);
|
||||
vi.mocked(db.commentVote.findMany).mockResolvedValue([
|
||||
{ value: 1 },
|
||||
{ value: -1 },
|
||||
] as never);
|
||||
vi.mocked(db.comment.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/comments/456/vote", {
|
||||
method: "DELETE",
|
||||
});
|
||||
await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123", commentId: "456" }),
|
||||
});
|
||||
|
||||
expect(db.comment.update).toHaveBeenCalledWith({
|
||||
where: { id: "456" },
|
||||
data: { score: 0 }, // 1 - 1 = 0
|
||||
});
|
||||
});
|
||||
});
|
||||
315
src/__tests__/api/pin.test.ts
Normal file
315
src/__tests__/api/pin.test.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST, DELETE } from "@/app/api/prompts/[id]/pin/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
prompt: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
pinnedPrompt: {
|
||||
findUnique: vi.fn(),
|
||||
count: vi.fn(),
|
||||
aggregate: vi.fn(),
|
||||
create: vi.fn(),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/prompts/[id]/pin", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 401 if session has no user id", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: {} } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("Prompt not found");
|
||||
});
|
||||
|
||||
it("should return 403 when pinning another user's prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "other-user",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("You can only pin your own prompts");
|
||||
});
|
||||
|
||||
it("should return 400 if prompt already pinned", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.findUnique).mockResolvedValue({
|
||||
userId: "user1",
|
||||
promptId: "123",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Prompt already pinned");
|
||||
});
|
||||
|
||||
it("should return 400 if pin limit (3) reached", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.pinnedPrompt.count).mockResolvedValue(3);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("You can only pin up to 3 prompts");
|
||||
});
|
||||
|
||||
it("should create pin with order 0 when no existing pins", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.pinnedPrompt.count).mockResolvedValue(0);
|
||||
vi.mocked(db.pinnedPrompt.aggregate).mockResolvedValue({
|
||||
_max: { order: null },
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.create).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.pinned).toBe(true);
|
||||
expect(db.pinnedPrompt.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userId: "user1",
|
||||
promptId: "123",
|
||||
order: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should increment order for subsequent pins", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.pinnedPrompt.count).mockResolvedValue(2);
|
||||
vi.mocked(db.pinnedPrompt.aggregate).mockResolvedValue({
|
||||
_max: { order: 1 },
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.create).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.pinnedPrompt.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
userId: "user1",
|
||||
promptId: "123",
|
||||
order: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return success: true, pinned: true on successful pin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
isPrivate: false,
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.pinnedPrompt.count).mockResolvedValue(0);
|
||||
vi.mocked(db.pinnedPrompt.aggregate).mockResolvedValue({
|
||||
_max: { order: null },
|
||||
} as never);
|
||||
vi.mocked(db.pinnedPrompt.create).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toEqual({ success: true, pinned: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/prompts/[id]/pin", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should remove pin successfully", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.pinnedPrompt.deleteMany).mockResolvedValue({ count: 1 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.pinned).toBe(false);
|
||||
});
|
||||
|
||||
it("should call deleteMany with correct parameters", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.pinnedPrompt.deleteMany).mockResolvedValue({ count: 1 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "DELETE",
|
||||
});
|
||||
await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.pinnedPrompt.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId: "user1",
|
||||
promptId: "123",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle unpinning non-pinned prompt gracefully", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.pinnedPrompt.deleteMany).mockResolvedValue({ count: 0 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toEqual({ success: true, pinned: false });
|
||||
});
|
||||
|
||||
it("should return success: true, pinned: false on successful unpin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.pinnedPrompt.deleteMany).mockResolvedValue({ count: 1 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/pin", {
|
||||
method: "DELETE",
|
||||
});
|
||||
const response = await DELETE(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toEqual({ success: true, pinned: false });
|
||||
});
|
||||
});
|
||||
381
src/__tests__/api/prompt-connections.test.ts
Normal file
381
src/__tests__/api/prompt-connections.test.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET, POST } from "@/app/api/prompts/[id]/connections/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
prompt: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
promptConnection: {
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/cache", () => ({
|
||||
revalidateTag: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/prompts/[id]/connections", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent prompt", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("Prompt not found");
|
||||
});
|
||||
|
||||
it("should return empty connections for prompt with none", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
id: "123",
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptConnection.findMany).mockResolvedValue([]);
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.outgoing).toEqual([]);
|
||||
expect(data.incoming).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return outgoing and incoming connections", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
id: "123",
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptConnection.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "conn1",
|
||||
label: "next",
|
||||
order: 0,
|
||||
target: { id: "target1", title: "Target Prompt", slug: "target", isPrivate: false, authorId: "user1" },
|
||||
},
|
||||
] as never)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "conn2",
|
||||
label: "previous",
|
||||
order: 0,
|
||||
source: { id: "source1", title: "Source Prompt", slug: "source", isPrivate: false, authorId: "user2" },
|
||||
},
|
||||
] as never);
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.outgoing).toHaveLength(1);
|
||||
expect(data.incoming).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("should filter out private prompts the user cannot see", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
id: "123",
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptConnection.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "conn1",
|
||||
label: "next",
|
||||
target: { id: "target1", title: "Private", slug: "private", isPrivate: true, authorId: "other-user" },
|
||||
},
|
||||
] as never)
|
||||
.mockResolvedValueOnce([]);
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(data.outgoing).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should show private prompts owned by the user", async () => {
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
id: "123",
|
||||
isPrivate: false,
|
||||
authorId: "user1",
|
||||
} as never);
|
||||
vi.mocked(db.promptConnection.findMany)
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "conn1",
|
||||
label: "next",
|
||||
target: { id: "target1", title: "My Private", slug: "private", isPrivate: true, authorId: "user1" },
|
||||
},
|
||||
] as never)
|
||||
.mockResolvedValueOnce([]);
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(data.outgoing).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/prompts/[id]/connections", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 404 if source prompt not found", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("Source prompt not found");
|
||||
});
|
||||
|
||||
it("should return 403 if user does not own source prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ authorId: "other-user" } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("You can only add connections to your own prompts");
|
||||
});
|
||||
|
||||
it("should return 404 if target prompt not found", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never) // Source
|
||||
.mockResolvedValueOnce(null); // Target
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("Target prompt not found");
|
||||
});
|
||||
|
||||
it("should return 403 if user does not own target prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never) // Source
|
||||
.mockResolvedValueOnce({ id: "456", authorId: "other-user" } as never); // Target
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("You can only connect to your own prompts");
|
||||
});
|
||||
|
||||
it("should return 400 for self-connection", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never)
|
||||
.mockResolvedValueOnce({ id: "123", authorId: "user1" } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "123", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Cannot connect a prompt to itself");
|
||||
});
|
||||
|
||||
it("should return 400 if connection already exists", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never)
|
||||
.mockResolvedValueOnce({ id: "456", authorId: "user1" } as never);
|
||||
vi.mocked(db.promptConnection.findUnique).mockResolvedValue({ id: "existing" } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Connection already exists");
|
||||
});
|
||||
|
||||
it("should create connection successfully", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never)
|
||||
.mockResolvedValueOnce({ id: "456", authorId: "user1" } as never);
|
||||
vi.mocked(db.promptConnection.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.promptConnection.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.promptConnection.create).mockResolvedValue({
|
||||
id: "conn1",
|
||||
sourceId: "123",
|
||||
targetId: "456",
|
||||
label: "next",
|
||||
order: 0,
|
||||
target: { id: "456", title: "Target", slug: "target" },
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.label).toBe("next");
|
||||
});
|
||||
|
||||
it("should auto-increment order when not provided", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "user1" } as never)
|
||||
.mockResolvedValueOnce({ id: "456", authorId: "user1" } as never);
|
||||
vi.mocked(db.promptConnection.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.promptConnection.findFirst).mockResolvedValue({ order: 2 } as never);
|
||||
vi.mocked(db.promptConnection.create).mockResolvedValue({
|
||||
id: "conn1",
|
||||
order: 3,
|
||||
target: {},
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "next" }),
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.promptConnection.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ order: 3 }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 400 for missing required fields", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456" }), // Missing label
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it("should allow admin to create connections for any prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique)
|
||||
.mockResolvedValueOnce({ authorId: "other-user" } as never)
|
||||
.mockResolvedValueOnce({ id: "456", authorId: "another-user" } as never);
|
||||
vi.mocked(db.promptConnection.findUnique).mockResolvedValue(null);
|
||||
vi.mocked(db.promptConnection.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.promptConnection.create).mockResolvedValue({
|
||||
id: "conn1",
|
||||
target: {},
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/connections", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ targetId: "456", label: "admin-link" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
});
|
||||
});
|
||||
157
src/__tests__/api/prompt-feature.test.ts
Normal file
157
src/__tests__/api/prompt-feature.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/app/api/prompts/[id]/feature/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
},
|
||||
prompt: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/prompts/[id]/feature", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "USER" } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("Forbidden");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "ADMIN" } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("Prompt not found");
|
||||
});
|
||||
|
||||
it("should toggle featured status from false to true", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "ADMIN" } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ isFeatured: false } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isFeatured: true } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.isFeatured).toBe(true);
|
||||
});
|
||||
|
||||
it("should toggle featured status from true to false", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "ADMIN" } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ isFeatured: true } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isFeatured: false } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.isFeatured).toBe(false);
|
||||
});
|
||||
|
||||
it("should set featuredAt when featuring a prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "ADMIN" } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ isFeatured: false } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isFeatured: true } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.prompt.update).toHaveBeenCalledWith({
|
||||
where: { id: "123" },
|
||||
data: {
|
||||
isFeatured: true,
|
||||
featuredAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear featuredAt when unfeaturing a prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({ role: "ADMIN" } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ isFeatured: true } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isFeatured: false } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/feature", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.prompt.update).toHaveBeenCalledWith({
|
||||
where: { id: "123" },
|
||||
data: {
|
||||
isFeatured: false,
|
||||
featuredAt: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
154
src/__tests__/api/prompt-unlist.test.ts
Normal file
154
src/__tests__/api/prompt-unlist.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { POST } from "@/app/api/prompts/[id]/unlist/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
prompt: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next/cache", () => ({
|
||||
revalidateTag: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/prompts/[id]/unlist", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 403 if user is not admin", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1", role: "USER" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should toggle unlisted status from false to true", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ id: "123", isUnlisted: false } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isUnlisted: true } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.isUnlisted).toBe(true);
|
||||
expect(data.message).toBe("Prompt unlisted");
|
||||
});
|
||||
|
||||
it("should toggle unlisted status from true to false (relist)", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ id: "123", isUnlisted: true } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({ isUnlisted: false } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(data.isUnlisted).toBe(false);
|
||||
expect(data.message).toBe("Prompt relisted");
|
||||
});
|
||||
|
||||
it("should set unlistedAt when unlisting", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ id: "123", isUnlisted: false } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.prompt.update).toHaveBeenCalledWith({
|
||||
where: { id: "123" },
|
||||
data: {
|
||||
isUnlisted: true,
|
||||
unlistedAt: expect.any(Date),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should clear unlistedAt when relisting", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "admin1", role: "ADMIN" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({ id: "123", isUnlisted: true } as never);
|
||||
vi.mocked(db.prompt.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/unlist", {
|
||||
method: "POST",
|
||||
});
|
||||
await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.prompt.update).toHaveBeenCalledWith({
|
||||
where: { id: "123" },
|
||||
data: {
|
||||
isUnlisted: false,
|
||||
unlistedAt: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
294
src/__tests__/api/user-api-key.test.ts
Normal file
294
src/__tests__/api/user-api-key.test.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET, POST, DELETE, PATCH } from "@/app/api/user/api-key/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { generateApiKey } from "@/lib/api-key";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
user: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/api-key", () => ({
|
||||
generateApiKey: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/user/api-key", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 401 if session has no user id", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: {} } as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 404 if user not found", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue(null);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("User not found");
|
||||
});
|
||||
|
||||
it("should return hasApiKey: false when no key exists", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({
|
||||
apiKey: null,
|
||||
mcpPromptsPublicByDefault: true,
|
||||
} as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.hasApiKey).toBe(false);
|
||||
expect(data.apiKey).toBeNull();
|
||||
});
|
||||
|
||||
it("should return hasApiKey: true and key when exists", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({
|
||||
apiKey: "pchat_abc123def456",
|
||||
mcpPromptsPublicByDefault: true,
|
||||
} as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.hasApiKey).toBe(true);
|
||||
expect(data.apiKey).toBe("pchat_abc123def456");
|
||||
});
|
||||
|
||||
it("should return mcpPromptsPublicByDefault setting", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.findUnique).mockResolvedValue({
|
||||
apiKey: "pchat_abc123",
|
||||
mcpPromptsPublicByDefault: false,
|
||||
} as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.mcpPromptsPublicByDefault).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/user/api-key", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const response = await POST();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should generate and return new API key", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(generateApiKey).mockReturnValue("pchat_newkey123");
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
const response = await POST();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.apiKey).toBe("pchat_newkey123");
|
||||
});
|
||||
|
||||
it("should update user with new key", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(generateApiKey).mockReturnValue("pchat_newkey123");
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
await POST();
|
||||
|
||||
expect(db.user.update).toHaveBeenCalledWith({
|
||||
where: { id: "user1" },
|
||||
data: { apiKey: "pchat_newkey123" },
|
||||
});
|
||||
});
|
||||
|
||||
it("should call generateApiKey function", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(generateApiKey).mockReturnValue("pchat_test");
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
await POST();
|
||||
|
||||
expect(generateApiKey).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/user/api-key", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const response = await DELETE();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should set apiKey to null", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
await DELETE();
|
||||
|
||||
expect(db.user.update).toHaveBeenCalledWith({
|
||||
where: { id: "user1" },
|
||||
data: { apiKey: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("should return success: true", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
const response = await DELETE();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/user/api-key", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ mcpPromptsPublicByDefault: true }),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should return 400 for missing mcpPromptsPublicByDefault", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Invalid request");
|
||||
});
|
||||
|
||||
it("should return 400 for non-boolean value", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ mcpPromptsPublicByDefault: "true" }),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Invalid request");
|
||||
});
|
||||
|
||||
it("should return 400 for number value", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ mcpPromptsPublicByDefault: 1 }),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("Invalid request");
|
||||
});
|
||||
|
||||
it("should update mcpPromptsPublicByDefault to true", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ mcpPromptsPublicByDefault: true }),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(db.user.update).toHaveBeenCalledWith({
|
||||
where: { id: "user1" },
|
||||
data: { mcpPromptsPublicByDefault: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("should update mcpPromptsPublicByDefault to false", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.user.update).mockResolvedValue({} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/api-key", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ mcpPromptsPublicByDefault: false }),
|
||||
});
|
||||
const response = await PATCH(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(db.user.update).toHaveBeenCalledWith({
|
||||
where: { id: "user1" },
|
||||
data: { mcpPromptsPublicByDefault: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
210
src/__tests__/api/user-notifications.test.ts
Normal file
210
src/__tests__/api/user-notifications.test.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET, POST } from "@/app/api/user/notifications/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
changeRequest: {
|
||||
count: vi.fn(),
|
||||
},
|
||||
notification: {
|
||||
findMany: vi.fn(),
|
||||
updateMany: vi.fn(),
|
||||
},
|
||||
prompt: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/user/notifications", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return default response if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toEqual({
|
||||
pendingChangeRequests: 0,
|
||||
unreadComments: 0,
|
||||
commentNotifications: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should return default response if session has no user id", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: {} } as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.pendingChangeRequests).toBe(0);
|
||||
expect(data.unreadComments).toBe(0);
|
||||
});
|
||||
|
||||
it("should return notifications for authenticated user", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.changeRequest.count).mockResolvedValue(3);
|
||||
vi.mocked(db.notification.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "notif1",
|
||||
type: "COMMENT",
|
||||
createdAt: new Date(),
|
||||
promptId: "prompt1",
|
||||
actor: {
|
||||
id: "user2",
|
||||
name: "Commenter",
|
||||
username: "commenter",
|
||||
avatar: null,
|
||||
},
|
||||
},
|
||||
] as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([
|
||||
{ id: "prompt1", title: "Test Prompt" },
|
||||
] as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.pendingChangeRequests).toBe(3);
|
||||
expect(data.unreadComments).toBe(1);
|
||||
expect(data.commentNotifications).toHaveLength(1);
|
||||
expect(data.commentNotifications[0].promptTitle).toBe("Test Prompt");
|
||||
});
|
||||
|
||||
it("should include actor info in notifications", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.changeRequest.count).mockResolvedValue(0);
|
||||
vi.mocked(db.notification.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "notif1",
|
||||
type: "REPLY",
|
||||
createdAt: new Date(),
|
||||
promptId: "prompt1",
|
||||
actor: {
|
||||
id: "user2",
|
||||
name: "Reply User",
|
||||
username: "replyuser",
|
||||
avatar: "avatar.jpg",
|
||||
},
|
||||
},
|
||||
] as never);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([
|
||||
{ id: "prompt1", title: "My Prompt" },
|
||||
] as never);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data.commentNotifications[0].actor.name).toBe("Reply User");
|
||||
expect(data.commentNotifications[0].actor.avatar).toBe("avatar.jpg");
|
||||
});
|
||||
|
||||
it("should return empty notifications array when none exist", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.changeRequest.count).mockResolvedValue(0);
|
||||
vi.mocked(db.notification.findMany).mockResolvedValue([]);
|
||||
vi.mocked(db.prompt.findMany).mockResolvedValue([]);
|
||||
|
||||
const response = await GET();
|
||||
const data = await response.json();
|
||||
|
||||
expect(data.commentNotifications).toEqual([]);
|
||||
expect(data.unreadComments).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/user/notifications", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("Unauthorized");
|
||||
});
|
||||
|
||||
it("should mark specific notifications as read", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.notification.updateMany).mockResolvedValue({ count: 2 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ notificationIds: ["notif1", "notif2"] }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(db.notification.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: { in: ["notif1", "notif2"] },
|
||||
userId: "user1",
|
||||
},
|
||||
data: { read: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark all notifications as read when no ids provided", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.notification.updateMany).mockResolvedValue({ count: 5 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.success).toBe(true);
|
||||
expect(db.notification.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId: "user1",
|
||||
read: false,
|
||||
},
|
||||
data: { read: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("should mark all notifications as read when notificationIds is not an array", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.notification.updateMany).mockResolvedValue({ count: 3 } as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/user/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ notificationIds: "invalid" }),
|
||||
});
|
||||
const response = await POST(request);
|
||||
|
||||
expect(db.notification.updateMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
userId: "user1",
|
||||
read: false,
|
||||
},
|
||||
data: { read: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
412
src/__tests__/api/versions.test.ts
Normal file
412
src/__tests__/api/versions.test.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { GET, POST } from "@/app/api/prompts/[id]/versions/route";
|
||||
import { db } from "@/lib/db";
|
||||
import { auth } from "@/lib/auth";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@/lib/db", () => ({
|
||||
db: {
|
||||
prompt: {
|
||||
findUnique: vi.fn(),
|
||||
update: vi.fn(),
|
||||
},
|
||||
promptVersion: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
create: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/auth", () => ({
|
||||
auth: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("GET /api/prompts/[id]/versions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return empty array for prompt with no versions", async () => {
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([]);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return versions ordered by version desc", async () => {
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "v3",
|
||||
version: 3,
|
||||
content: "Version 3 content",
|
||||
changeNote: "Version 3",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
{
|
||||
id: "v2",
|
||||
version: 2,
|
||||
content: "Version 2 content",
|
||||
changeNote: "Version 2",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "Version 1 content",
|
||||
changeNote: "Version 1",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data).toHaveLength(3);
|
||||
expect(data[0].version).toBe(3);
|
||||
expect(data[1].version).toBe(2);
|
||||
expect(data[2].version).toBe(1);
|
||||
});
|
||||
|
||||
it("should include author info in response", async () => {
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "Content",
|
||||
changeNote: "Initial",
|
||||
createdAt: new Date(),
|
||||
author: { name: "Test User", username: "testuser" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
const response = await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data[0].author.name).toBe("Test User");
|
||||
expect(data[0].author.username).toBe("testuser");
|
||||
});
|
||||
|
||||
it("should call findMany with correct parameters", async () => {
|
||||
vi.mocked(db.promptVersion.findMany).mockResolvedValue([]);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions");
|
||||
await GET(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
|
||||
expect(db.promptVersion.findMany).toHaveBeenCalledWith({
|
||||
where: { promptId: "123" },
|
||||
orderBy: { version: "desc" },
|
||||
include: {
|
||||
author: {
|
||||
select: {
|
||||
name: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/prompts/[id]/versions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return 401 if not authenticated", async () => {
|
||||
vi.mocked(auth).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(data.error).toBe("unauthorized");
|
||||
});
|
||||
|
||||
it("should return 404 for non-existent prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue(null);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(data.error).toBe("not_found");
|
||||
});
|
||||
|
||||
it("should return 403 if user does not own the prompt", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "other-user",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(data.error).toBe("forbidden");
|
||||
});
|
||||
|
||||
it("should return 400 for empty content", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 400 for missing content", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("validation_error");
|
||||
});
|
||||
|
||||
it("should return 400 when content is same as current version", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Same content",
|
||||
} as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "Same content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe("no_change");
|
||||
});
|
||||
|
||||
it("should create version with incrementing version number", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findFirst).mockResolvedValue({
|
||||
version: 2,
|
||||
} as never);
|
||||
vi.mocked(db.$transaction).mockResolvedValue([
|
||||
{
|
||||
id: "v3",
|
||||
version: 3,
|
||||
content: "New content",
|
||||
changeNote: "Version 3",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.version).toBe(3);
|
||||
});
|
||||
|
||||
it("should use default changeNote when not provided", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.$transaction).mockImplementation(async (ops) => {
|
||||
// Capture the create call to verify changeNote
|
||||
return [
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "New content",
|
||||
changeNote: "Version 1",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.changeNote).toBe("Version 1");
|
||||
});
|
||||
|
||||
it("should use custom changeNote when provided", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.$transaction).mockResolvedValue([
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "New content",
|
||||
changeNote: "Fixed typo in instructions",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
content: "New content",
|
||||
changeNote: "Fixed typo in instructions",
|
||||
}),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.changeNote).toBe("Fixed typo in instructions");
|
||||
});
|
||||
|
||||
it("should start at version 1 when no previous versions exist", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.$transaction).mockResolvedValue([
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "New content",
|
||||
changeNote: "Version 1",
|
||||
createdAt: new Date(),
|
||||
author: { name: "User", username: "user" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.version).toBe(1);
|
||||
});
|
||||
|
||||
it("should return created version with author info", async () => {
|
||||
vi.mocked(auth).mockResolvedValue({ user: { id: "user1" } } as never);
|
||||
vi.mocked(db.prompt.findUnique).mockResolvedValue({
|
||||
authorId: "user1",
|
||||
content: "Original content",
|
||||
} as never);
|
||||
vi.mocked(db.promptVersion.findFirst).mockResolvedValue(null);
|
||||
vi.mocked(db.$transaction).mockResolvedValue([
|
||||
{
|
||||
id: "v1",
|
||||
version: 1,
|
||||
content: "New content",
|
||||
changeNote: "Version 1",
|
||||
createdAt: new Date(),
|
||||
author: { name: "Test User", username: "testuser" },
|
||||
},
|
||||
] as never);
|
||||
|
||||
const request = new Request("http://localhost:3000/api/prompts/123/versions", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ content: "New content" }),
|
||||
});
|
||||
const response = await POST(request, {
|
||||
params: Promise.resolve({ id: "123" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(data.author.name).toBe("Test User");
|
||||
expect(data.author.username).toBe("testuser");
|
||||
});
|
||||
});
|
||||
228
src/__tests__/hooks/use-debounce.test.ts
Normal file
228
src/__tests__/hooks/use-debounce.test.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useDebounce } from "@/lib/hooks/use-debounce";
|
||||
|
||||
describe("useDebounce", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("returns initial value immediately", () => {
|
||||
const { result } = renderHook(() => useDebounce("initial", 500));
|
||||
expect(result.current).toBe("initial");
|
||||
});
|
||||
|
||||
it("does not update value before delay", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 500 } }
|
||||
);
|
||||
|
||||
rerender({ value: "updated", delay: 500 });
|
||||
|
||||
// Advance time, but not past the delay
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(result.current).toBe("initial");
|
||||
});
|
||||
|
||||
it("updates value after delay", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 500 } }
|
||||
);
|
||||
|
||||
rerender({ value: "updated", delay: 500 });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(result.current).toBe("updated");
|
||||
});
|
||||
|
||||
it("resets timer on rapid value changes", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 500 } }
|
||||
);
|
||||
|
||||
// First update
|
||||
rerender({ value: "update1", delay: 500 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
expect(result.current).toBe("initial");
|
||||
|
||||
// Second update before delay completes
|
||||
rerender({ value: "update2", delay: 500 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
expect(result.current).toBe("initial");
|
||||
|
||||
// Complete delay for second update
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
expect(result.current).toBe("update2");
|
||||
});
|
||||
|
||||
it("works with different data types", () => {
|
||||
// Number
|
||||
const { result: numResult, rerender: numRerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: 0, delay: 100 } }
|
||||
);
|
||||
|
||||
numRerender({ value: 42, delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(numResult.current).toBe(42);
|
||||
|
||||
// Object
|
||||
const initialObj = { key: "value" };
|
||||
const updatedObj = { key: "updated" };
|
||||
|
||||
const { result: objResult, rerender: objRerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: initialObj, delay: 100 } }
|
||||
);
|
||||
|
||||
objRerender({ value: updatedObj, delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(objResult.current).toEqual(updatedObj);
|
||||
|
||||
// Array
|
||||
const { result: arrResult, rerender: arrRerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: [1, 2, 3], delay: 100 } }
|
||||
);
|
||||
|
||||
arrRerender({ value: [4, 5, 6], delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(arrResult.current).toEqual([4, 5, 6]);
|
||||
});
|
||||
|
||||
it("works with boolean values", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: false, delay: 100 } }
|
||||
);
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
rerender({ value: true, delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("works with null and undefined", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: null as string | null, delay: 100 } }
|
||||
);
|
||||
|
||||
expect(result.current).toBeNull();
|
||||
|
||||
rerender({ value: "defined", delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current).toBe("defined");
|
||||
|
||||
rerender({ value: null, delay: 100 });
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current).toBeNull();
|
||||
});
|
||||
|
||||
it("handles delay changes", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 500 } }
|
||||
);
|
||||
|
||||
// Change value and delay simultaneously
|
||||
rerender({ value: "updated", delay: 200 });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(200);
|
||||
});
|
||||
expect(result.current).toBe("updated");
|
||||
});
|
||||
|
||||
it("handles zero delay", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 0 } }
|
||||
);
|
||||
|
||||
rerender({ value: "updated", delay: 0 });
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(0);
|
||||
});
|
||||
expect(result.current).toBe("updated");
|
||||
});
|
||||
|
||||
it("cleans up timeout on unmount", () => {
|
||||
const clearTimeoutSpy = vi.spyOn(global, "clearTimeout");
|
||||
|
||||
const { unmount, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "initial", delay: 500 } }
|
||||
);
|
||||
|
||||
rerender({ value: "updated", delay: 500 });
|
||||
unmount();
|
||||
|
||||
expect(clearTimeoutSpy).toHaveBeenCalled();
|
||||
clearTimeoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("real-world search scenarios", () => {
|
||||
it("debounces search input effectively", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ value, delay }) => useDebounce(value, delay),
|
||||
{ initialProps: { value: "", delay: 300 } }
|
||||
);
|
||||
|
||||
// Simulate typing "hello"
|
||||
rerender({ value: "h", delay: 300 });
|
||||
act(() => vi.advanceTimersByTime(50));
|
||||
|
||||
rerender({ value: "he", delay: 300 });
|
||||
act(() => vi.advanceTimersByTime(50));
|
||||
|
||||
rerender({ value: "hel", delay: 300 });
|
||||
act(() => vi.advanceTimersByTime(50));
|
||||
|
||||
rerender({ value: "hell", delay: 300 });
|
||||
act(() => vi.advanceTimersByTime(50));
|
||||
|
||||
rerender({ value: "hello", delay: 300 });
|
||||
|
||||
// Still initial value (user typing fast)
|
||||
expect(result.current).toBe("");
|
||||
|
||||
// Wait for debounce
|
||||
act(() => vi.advanceTimersByTime(300));
|
||||
expect(result.current).toBe("hello");
|
||||
});
|
||||
});
|
||||
});
|
||||
528
src/__tests__/lib/skill-files.test.ts
Normal file
528
src/__tests__/lib/skill-files.test.ts
Normal file
@@ -0,0 +1,528 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
parseSkillFiles,
|
||||
serializeSkillFiles,
|
||||
getLanguageFromFilename,
|
||||
validateFilename,
|
||||
isValidKebabCase,
|
||||
parseSkillFrontmatter,
|
||||
validateSkillFrontmatter,
|
||||
generateSkillContentWithFrontmatter,
|
||||
suggestFilename,
|
||||
DEFAULT_SKILL_FILE,
|
||||
DEFAULT_SKILL_CONTENT,
|
||||
type SkillFile,
|
||||
} from "@/lib/skill-files";
|
||||
|
||||
describe("parseSkillFiles", () => {
|
||||
it("returns default content for empty string", () => {
|
||||
const result = parseSkillFiles("");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
|
||||
expect(result[0].content).toBe(DEFAULT_SKILL_CONTENT);
|
||||
});
|
||||
|
||||
it("returns default content for whitespace-only string", () => {
|
||||
const result = parseSkillFiles(" \n\t ");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
|
||||
});
|
||||
|
||||
it("parses single file content without separators", () => {
|
||||
const content = "# My Skill\n\nSome content here";
|
||||
const result = parseSkillFiles(content);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
|
||||
expect(result[0].content).toBe(content);
|
||||
});
|
||||
|
||||
it("parses multiple files with separators", () => {
|
||||
const content = "# Main Skill\n\x1FFILE:helper.ts\x1E\nconst x = 1;\n\x1FFILE:config.json\x1E\n{}";
|
||||
const result = parseSkillFiles(content);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
|
||||
expect(result[0].content).toBe("# Main Skill");
|
||||
expect(result[1].filename).toBe("helper.ts");
|
||||
expect(result[1].content).toBe("const x = 1;");
|
||||
expect(result[2].filename).toBe("config.json");
|
||||
expect(result[2].content).toBe("{}");
|
||||
});
|
||||
|
||||
it("ignores SKILL.md in file separators (already handled as first file)", () => {
|
||||
const content = "# Main\n\x1FFILE:SKILL.md\x1E\nshould be ignored";
|
||||
const result = parseSkillFiles(content);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].filename).toBe(DEFAULT_SKILL_FILE);
|
||||
});
|
||||
|
||||
it("handles empty file content after separator", () => {
|
||||
const content = "# Main\n\x1FFILE:empty.ts\x1E\n";
|
||||
const result = parseSkillFiles(content);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[1].filename).toBe("empty.ts");
|
||||
expect(result[1].content).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("serializeSkillFiles", () => {
|
||||
it("returns default content for empty array", () => {
|
||||
expect(serializeSkillFiles([])).toBe(DEFAULT_SKILL_CONTENT);
|
||||
});
|
||||
|
||||
it("serializes single SKILL.md file", () => {
|
||||
const files: SkillFile[] = [
|
||||
{ filename: DEFAULT_SKILL_FILE, content: "# My Skill" }
|
||||
];
|
||||
expect(serializeSkillFiles(files)).toBe("# My Skill");
|
||||
});
|
||||
|
||||
it("serializes multiple files with separators", () => {
|
||||
const files: SkillFile[] = [
|
||||
{ filename: DEFAULT_SKILL_FILE, content: "# Main" },
|
||||
{ filename: "helper.ts", content: "const x = 1;" },
|
||||
];
|
||||
const result = serializeSkillFiles(files);
|
||||
|
||||
expect(result).toContain("# Main");
|
||||
expect(result).toContain("\x1FFILE:helper.ts\x1E");
|
||||
expect(result).toContain("const x = 1;");
|
||||
});
|
||||
|
||||
it("puts SKILL.md content first regardless of array order", () => {
|
||||
const files: SkillFile[] = [
|
||||
{ filename: "other.ts", content: "other" },
|
||||
{ filename: DEFAULT_SKILL_FILE, content: "# Main" },
|
||||
];
|
||||
const result = serializeSkillFiles(files);
|
||||
|
||||
expect(result.startsWith("# Main")).toBe(true);
|
||||
});
|
||||
|
||||
it("uses default content if SKILL.md is missing", () => {
|
||||
const files: SkillFile[] = [
|
||||
{ filename: "other.ts", content: "other" }
|
||||
];
|
||||
const result = serializeSkillFiles(files);
|
||||
|
||||
expect(result.startsWith(DEFAULT_SKILL_CONTENT)).toBe(true);
|
||||
});
|
||||
|
||||
it("is reversible with parseSkillFiles", () => {
|
||||
const files: SkillFile[] = [
|
||||
{ filename: DEFAULT_SKILL_FILE, content: "# Main Skill" },
|
||||
{ filename: "helper.ts", content: "export const x = 1;" },
|
||||
{ filename: "config.json", content: '{ "key": "value" }' },
|
||||
];
|
||||
|
||||
const serialized = serializeSkillFiles(files);
|
||||
const parsed = parseSkillFiles(serialized);
|
||||
|
||||
expect(parsed).toHaveLength(3);
|
||||
expect(parsed[0]).toEqual(files[0]);
|
||||
expect(parsed[1]).toEqual(files[1]);
|
||||
expect(parsed[2]).toEqual(files[2]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLanguageFromFilename", () => {
|
||||
describe("markdown files", () => {
|
||||
it("detects .md files", () => {
|
||||
expect(getLanguageFromFilename("README.md")).toBe("markdown");
|
||||
expect(getLanguageFromFilename("SKILL.md")).toBe("markdown");
|
||||
});
|
||||
|
||||
it("detects .mdx files", () => {
|
||||
expect(getLanguageFromFilename("page.mdx")).toBe("markdown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript/TypeScript", () => {
|
||||
it("detects JavaScript files", () => {
|
||||
expect(getLanguageFromFilename("index.js")).toBe("javascript");
|
||||
expect(getLanguageFromFilename("component.jsx")).toBe("javascript");
|
||||
expect(getLanguageFromFilename("module.mjs")).toBe("javascript");
|
||||
expect(getLanguageFromFilename("common.cjs")).toBe("javascript");
|
||||
});
|
||||
|
||||
it("detects TypeScript files", () => {
|
||||
expect(getLanguageFromFilename("index.ts")).toBe("typescript");
|
||||
expect(getLanguageFromFilename("component.tsx")).toBe("typescript");
|
||||
});
|
||||
});
|
||||
|
||||
describe("web languages", () => {
|
||||
it("detects HTML files", () => {
|
||||
expect(getLanguageFromFilename("index.html")).toBe("html");
|
||||
expect(getLanguageFromFilename("page.htm")).toBe("html");
|
||||
});
|
||||
|
||||
it("detects CSS files", () => {
|
||||
expect(getLanguageFromFilename("styles.css")).toBe("css");
|
||||
expect(getLanguageFromFilename("styles.scss")).toBe("scss");
|
||||
expect(getLanguageFromFilename("styles.less")).toBe("less");
|
||||
});
|
||||
});
|
||||
|
||||
describe("data formats", () => {
|
||||
it("detects JSON files", () => {
|
||||
expect(getLanguageFromFilename("config.json")).toBe("json");
|
||||
});
|
||||
|
||||
it("detects YAML files", () => {
|
||||
expect(getLanguageFromFilename("config.yaml")).toBe("yaml");
|
||||
expect(getLanguageFromFilename("config.yml")).toBe("yaml");
|
||||
});
|
||||
|
||||
it("detects XML files", () => {
|
||||
expect(getLanguageFromFilename("data.xml")).toBe("xml");
|
||||
});
|
||||
|
||||
it("detects TOML files", () => {
|
||||
expect(getLanguageFromFilename("config.toml")).toBe("toml");
|
||||
});
|
||||
});
|
||||
|
||||
describe("programming languages", () => {
|
||||
it("detects Python files", () => {
|
||||
expect(getLanguageFromFilename("script.py")).toBe("python");
|
||||
});
|
||||
|
||||
it("detects Go files", () => {
|
||||
expect(getLanguageFromFilename("main.go")).toBe("go");
|
||||
});
|
||||
|
||||
it("detects Rust files", () => {
|
||||
expect(getLanguageFromFilename("lib.rs")).toBe("rust");
|
||||
});
|
||||
|
||||
it("detects Java files", () => {
|
||||
expect(getLanguageFromFilename("Main.java")).toBe("java");
|
||||
});
|
||||
|
||||
it("detects C/C++ files", () => {
|
||||
expect(getLanguageFromFilename("main.c")).toBe("c");
|
||||
expect(getLanguageFromFilename("main.cpp")).toBe("cpp");
|
||||
expect(getLanguageFromFilename("header.h")).toBe("c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shell and config files", () => {
|
||||
it("detects shell files", () => {
|
||||
expect(getLanguageFromFilename("script.sh")).toBe("shell");
|
||||
expect(getLanguageFromFilename("script.bash")).toBe("shell");
|
||||
expect(getLanguageFromFilename(".env")).toBe("shell");
|
||||
});
|
||||
|
||||
it("detects config files", () => {
|
||||
expect(getLanguageFromFilename("settings.ini")).toBe("ini");
|
||||
expect(getLanguageFromFilename(".editorconfig")).toBe("ini");
|
||||
});
|
||||
});
|
||||
|
||||
describe("special filenames", () => {
|
||||
it("detects Dockerfile", () => {
|
||||
expect(getLanguageFromFilename("Dockerfile")).toBe("dockerfile");
|
||||
expect(getLanguageFromFilename("dockerfile")).toBe("dockerfile");
|
||||
expect(getLanguageFromFilename("Dockerfile.dev")).toBe("dockerfile");
|
||||
});
|
||||
|
||||
it("detects Makefile", () => {
|
||||
expect(getLanguageFromFilename("Makefile")).toBe("makefile");
|
||||
expect(getLanguageFromFilename("makefile")).toBe("makefile");
|
||||
expect(getLanguageFromFilename("GNUmakefile")).toBe("makefile");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unknown extensions", () => {
|
||||
it("returns plaintext for unknown extensions", () => {
|
||||
expect(getLanguageFromFilename("file.unknown")).toBe("plaintext");
|
||||
expect(getLanguageFromFilename("noextension")).toBe("plaintext");
|
||||
});
|
||||
|
||||
it("returns plaintext for .txt files", () => {
|
||||
expect(getLanguageFromFilename("notes.txt")).toBe("plaintext");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateFilename", () => {
|
||||
const existingFiles = ["existing.ts", "config.json"];
|
||||
|
||||
describe("empty and invalid input", () => {
|
||||
it("rejects empty filename", () => {
|
||||
expect(validateFilename("", existingFiles)).toBe("filenameEmpty");
|
||||
expect(validateFilename(" ", existingFiles)).toBe("filenameEmpty");
|
||||
});
|
||||
|
||||
it("rejects invalid characters", () => {
|
||||
expect(validateFilename("file<name>.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename("file:name.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename('file"name.ts', existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename("file|name.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename("file?name.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename("file*name.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
expect(validateFilename("file\\name.ts", existingFiles)).toBe("filenameInvalidChars");
|
||||
});
|
||||
});
|
||||
|
||||
describe("path validation", () => {
|
||||
it("rejects paths starting with slash", () => {
|
||||
expect(validateFilename("/src/file.ts", existingFiles)).toBe("pathStartEndSlash");
|
||||
});
|
||||
|
||||
it("rejects paths ending with slash", () => {
|
||||
expect(validateFilename("src/file/", existingFiles)).toBe("pathStartEndSlash");
|
||||
});
|
||||
|
||||
it("rejects consecutive slashes", () => {
|
||||
expect(validateFilename("src//file.ts", existingFiles)).toBe("pathConsecutiveSlashes");
|
||||
});
|
||||
|
||||
it("rejects parent directory references", () => {
|
||||
expect(validateFilename("../file.ts", existingFiles)).toBe("pathContainsDotDot");
|
||||
expect(validateFilename("src/../file.ts", existingFiles)).toBe("pathContainsDotDot");
|
||||
});
|
||||
|
||||
it("allows valid directory paths", () => {
|
||||
expect(validateFilename("src/utils/helper.ts", existingFiles)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reserved names", () => {
|
||||
it("rejects SKILL.md as filename", () => {
|
||||
expect(validateFilename(DEFAULT_SKILL_FILE, existingFiles)).toBe("filenameReserved");
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicates", () => {
|
||||
it("rejects duplicate filenames (case-insensitive)", () => {
|
||||
expect(validateFilename("existing.ts", existingFiles)).toBe("filenameDuplicate");
|
||||
expect(validateFilename("EXISTING.TS", existingFiles)).toBe("filenameDuplicate");
|
||||
expect(validateFilename("Existing.Ts", existingFiles)).toBe("filenameDuplicate");
|
||||
});
|
||||
});
|
||||
|
||||
describe("length limits", () => {
|
||||
it("rejects paths over 200 characters", () => {
|
||||
const longPath = "a".repeat(201);
|
||||
expect(validateFilename(longPath, existingFiles)).toBe("pathTooLong");
|
||||
});
|
||||
|
||||
it("allows paths up to 200 characters", () => {
|
||||
const maxPath = "a".repeat(200);
|
||||
expect(validateFilename(maxPath, existingFiles)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("valid filenames", () => {
|
||||
it("accepts valid simple filenames", () => {
|
||||
expect(validateFilename("newfile.ts", existingFiles)).toBeNull();
|
||||
expect(validateFilename("readme.md", existingFiles)).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts valid paths with directories", () => {
|
||||
expect(validateFilename("src/components/button.tsx", existingFiles)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidKebabCase", () => {
|
||||
it("accepts valid kebab-case names", () => {
|
||||
expect(isValidKebabCase("my-skill")).toBe(true);
|
||||
expect(isValidKebabCase("another-great-skill")).toBe(true);
|
||||
expect(isValidKebabCase("skill123")).toBe(true);
|
||||
expect(isValidKebabCase("my-skill-v2")).toBe(true);
|
||||
});
|
||||
|
||||
it("requires starting with a letter", () => {
|
||||
expect(isValidKebabCase("123-skill")).toBe(false);
|
||||
expect(isValidKebabCase("-my-skill")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects uppercase letters", () => {
|
||||
expect(isValidKebabCase("My-Skill")).toBe(false);
|
||||
expect(isValidKebabCase("mySkill")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects underscores and spaces", () => {
|
||||
expect(isValidKebabCase("my_skill")).toBe(false);
|
||||
expect(isValidKebabCase("my skill")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects empty string", () => {
|
||||
expect(isValidKebabCase("")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseSkillFrontmatter", () => {
|
||||
it("parses valid frontmatter", () => {
|
||||
const content = `---
|
||||
name: my-skill
|
||||
description: A test skill
|
||||
---
|
||||
|
||||
# My Skill`;
|
||||
|
||||
const result = parseSkillFrontmatter(content);
|
||||
expect(result).toEqual({
|
||||
name: "my-skill",
|
||||
description: "A test skill",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for missing frontmatter", () => {
|
||||
const content = "# My Skill\n\nNo frontmatter here";
|
||||
expect(parseSkillFrontmatter(content)).toBeNull();
|
||||
});
|
||||
|
||||
it("handles partial frontmatter", () => {
|
||||
const content = `---
|
||||
name: my-skill
|
||||
---
|
||||
|
||||
# My Skill`;
|
||||
|
||||
const result = parseSkillFrontmatter(content);
|
||||
expect(result).toEqual({ name: "my-skill" });
|
||||
});
|
||||
|
||||
it("handles multi-file content", () => {
|
||||
const content = `---
|
||||
name: multi-file-skill
|
||||
description: Has multiple files
|
||||
---
|
||||
|
||||
# Main\n\x1FFILE:helper.ts\x1E\nconst x = 1;`;
|
||||
|
||||
const result = parseSkillFrontmatter(content);
|
||||
expect(result?.name).toBe("multi-file-skill");
|
||||
expect(result?.description).toBe("Has multiple files");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateSkillFrontmatter", () => {
|
||||
it("returns null for valid frontmatter", () => {
|
||||
const content = `---
|
||||
name: valid-skill
|
||||
description: A valid description
|
||||
---
|
||||
|
||||
# Content`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns error for missing frontmatter", () => {
|
||||
const content = "# No frontmatter";
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterMissing");
|
||||
});
|
||||
|
||||
it("returns error for missing name", () => {
|
||||
const content = `---
|
||||
description: Has description but no name
|
||||
---`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameRequired");
|
||||
});
|
||||
|
||||
it("returns error for default placeholder name", () => {
|
||||
const content = `---
|
||||
name: my-skill-name
|
||||
description: Real description
|
||||
---`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameRequired");
|
||||
});
|
||||
|
||||
it("returns error for invalid name format", () => {
|
||||
const content = `---
|
||||
name: Invalid Name
|
||||
description: Real description
|
||||
---`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterNameInvalidFormat");
|
||||
});
|
||||
|
||||
it("returns error for missing description", () => {
|
||||
const content = `---
|
||||
name: valid-name
|
||||
---`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterDescriptionRequired");
|
||||
});
|
||||
|
||||
it("returns error for default placeholder description", () => {
|
||||
const content = `---
|
||||
name: valid-name
|
||||
description: A clear description of what this skill does and when to use it
|
||||
---`;
|
||||
|
||||
expect(validateSkillFrontmatter(content)).toBe("frontmatterDescriptionRequired");
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateSkillContentWithFrontmatter", () => {
|
||||
it("generates content with frontmatter from title and description", () => {
|
||||
const result = generateSkillContentWithFrontmatter("My Test Skill", "A description");
|
||||
|
||||
expect(result).toContain("---");
|
||||
expect(result).toContain("name: my-test-skill");
|
||||
expect(result).toContain("description: A description");
|
||||
expect(result).toContain("# My Test Skill");
|
||||
});
|
||||
|
||||
it("converts title to kebab-case", () => {
|
||||
const result = generateSkillContentWithFrontmatter("Complex Title With Numbers 123", "desc");
|
||||
expect(result).toContain("name: complex-title-with-numbers-123");
|
||||
});
|
||||
|
||||
it("handles special characters in title", () => {
|
||||
const result = generateSkillContentWithFrontmatter("Café Helper", "desc");
|
||||
expect(result).toContain("name: cafe-helper");
|
||||
});
|
||||
|
||||
it("uses default description when empty", () => {
|
||||
const result = generateSkillContentWithFrontmatter("Test", "");
|
||||
expect(result).toContain("description: A clear description of what this skill does");
|
||||
});
|
||||
|
||||
it("uses default title when empty", () => {
|
||||
const result = generateSkillContentWithFrontmatter("", "desc");
|
||||
expect(result).toContain("# My Skill");
|
||||
});
|
||||
});
|
||||
|
||||
describe("suggestFilename", () => {
|
||||
it("suggests README.md first for empty list", () => {
|
||||
expect(suggestFilename([])).toBe("README.md");
|
||||
});
|
||||
|
||||
it("skips already existing files", () => {
|
||||
expect(suggestFilename(["README.md"])).toBe("config.json");
|
||||
expect(suggestFilename(["README.md", "config.json"])).toBe("schema.json");
|
||||
});
|
||||
|
||||
it("is case-insensitive when checking existing files", () => {
|
||||
expect(suggestFilename(["readme.md"])).toBe("config.json");
|
||||
expect(suggestFilename(["README.MD"])).toBe("config.json");
|
||||
});
|
||||
|
||||
it("falls back to numbered files when all suggestions taken", () => {
|
||||
const allTaken = [
|
||||
"README.md", "config.json", "schema.json", "template.md",
|
||||
"example.ts", "utils.ts", "types.ts", "constants.ts"
|
||||
];
|
||||
expect(suggestFilename(allTaken)).toBe("file1.md");
|
||||
});
|
||||
|
||||
it("increments number for fallback files", () => {
|
||||
const withFile1 = [
|
||||
"README.md", "config.json", "schema.json", "template.md",
|
||||
"example.ts", "utils.ts", "types.ts", "constants.ts", "file1.md"
|
||||
];
|
||||
expect(suggestFilename(withFile1)).toBe("file2.md");
|
||||
});
|
||||
});
|
||||
100
src/__tests__/lib/slug.test.ts
Normal file
100
src/__tests__/lib/slug.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { slugify } from "@/lib/slug";
|
||||
|
||||
describe("slugify", () => {
|
||||
describe("basic transformations", () => {
|
||||
it("converts text to lowercase", () => {
|
||||
expect(slugify("Hello World")).toBe("hello-world");
|
||||
expect(slugify("UPPERCASE")).toBe("uppercase");
|
||||
});
|
||||
|
||||
it("trims whitespace", () => {
|
||||
expect(slugify(" hello world ")).toBe("hello-world");
|
||||
expect(slugify("\thello\n")).toBe("hello");
|
||||
});
|
||||
|
||||
it("replaces spaces with hyphens", () => {
|
||||
expect(slugify("hello world")).toBe("hello-world");
|
||||
expect(slugify("multiple spaces")).toBe("multiple-spaces");
|
||||
});
|
||||
|
||||
it("replaces underscores with hyphens", () => {
|
||||
expect(slugify("hello_world")).toBe("hello-world");
|
||||
expect(slugify("multiple__underscores")).toBe("multiple-underscores");
|
||||
});
|
||||
});
|
||||
|
||||
describe("special character handling", () => {
|
||||
it("removes non-word characters", () => {
|
||||
expect(slugify("hello@world")).toBe("helloworld");
|
||||
expect(slugify("test!@#$%^&*()")).toBe("test");
|
||||
expect(slugify("hello.world")).toBe("helloworld");
|
||||
});
|
||||
|
||||
it("preserves hyphens in the middle", () => {
|
||||
expect(slugify("hello-world")).toBe("hello-world");
|
||||
expect(slugify("already-slugified")).toBe("already-slugified");
|
||||
});
|
||||
|
||||
it("removes leading and trailing hyphens", () => {
|
||||
expect(slugify("-hello-")).toBe("hello");
|
||||
expect(slugify("---test---")).toBe("test");
|
||||
expect(slugify("-multiple--hyphens-")).toBe("multiple-hyphens");
|
||||
});
|
||||
|
||||
it("collapses multiple hyphens into one", () => {
|
||||
expect(slugify("hello---world")).toBe("hello-world");
|
||||
expect(slugify("test - - test")).toBe("test-test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("length limiting", () => {
|
||||
it("limits output to 100 characters", () => {
|
||||
const longText = "a".repeat(150);
|
||||
expect(slugify(longText).length).toBe(100);
|
||||
});
|
||||
|
||||
it("preserves shorter text fully", () => {
|
||||
const shortText = "short title";
|
||||
expect(slugify(shortText)).toBe("short-title");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles empty string", () => {
|
||||
expect(slugify("")).toBe("");
|
||||
});
|
||||
|
||||
it("handles string with only special characters", () => {
|
||||
expect(slugify("@#$%^&*")).toBe("");
|
||||
});
|
||||
|
||||
it("handles mixed content", () => {
|
||||
expect(slugify("Hello, World! How are you?")).toBe("hello-world-how-are-you");
|
||||
});
|
||||
|
||||
it("handles numbers", () => {
|
||||
expect(slugify("Test 123")).toBe("test-123");
|
||||
expect(slugify("123 Test")).toBe("123-test");
|
||||
});
|
||||
|
||||
it("handles mixed case with numbers and special chars", () => {
|
||||
expect(slugify("Product #1: The Best!")).toBe("product-1-the-best");
|
||||
});
|
||||
});
|
||||
|
||||
describe("real-world examples", () => {
|
||||
it("handles prompt titles", () => {
|
||||
expect(slugify("Act as a JavaScript Console")).toBe("act-as-a-javascript-console");
|
||||
expect(slugify("Act as an English Translator")).toBe("act-as-an-english-translator");
|
||||
});
|
||||
|
||||
it("handles titles with quotes", () => {
|
||||
expect(slugify("Act as a \"Code Reviewer\"")).toBe("act-as-a-code-reviewer");
|
||||
});
|
||||
|
||||
it("handles titles with apostrophes", () => {
|
||||
expect(slugify("Developer's Guide")).toBe("developers-guide");
|
||||
});
|
||||
});
|
||||
});
|
||||
244
src/__tests__/lib/works-best-with.test.ts
Normal file
244
src/__tests__/lib/works-best-with.test.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
getModelInfo,
|
||||
isValidModelSlug,
|
||||
getModelsByProvider,
|
||||
validateBestWithModels,
|
||||
validateBestWithMCP,
|
||||
AI_MODELS,
|
||||
} from "@/lib/works-best-with";
|
||||
|
||||
describe("getModelInfo", () => {
|
||||
it("returns model info for valid slugs", () => {
|
||||
expect(getModelInfo("gpt-4o")).toEqual({ name: "GPT-4o", provider: "OpenAI" });
|
||||
expect(getModelInfo("claude-4-opus")).toEqual({ name: "Claude 4 Opus", provider: "Anthropic" });
|
||||
expect(getModelInfo("gemini-2-5-pro")).toEqual({ name: "Gemini 2.5 Pro", provider: "Google" });
|
||||
});
|
||||
|
||||
it("returns null for invalid slugs", () => {
|
||||
expect(getModelInfo("invalid-model")).toBeNull();
|
||||
expect(getModelInfo("")).toBeNull();
|
||||
expect(getModelInfo("gpt-99")).toBeNull();
|
||||
});
|
||||
|
||||
it("returns correct info for image generation models", () => {
|
||||
expect(getModelInfo("dall-e-3")).toEqual({ name: "DALL·E 3", provider: "OpenAI" });
|
||||
expect(getModelInfo("midjourney")).toEqual({ name: "Midjourney", provider: "Midjourney" });
|
||||
});
|
||||
|
||||
it("returns correct info for video generation models", () => {
|
||||
expect(getModelInfo("sora 2")).toEqual({ name: "Sora 2", provider: "OpenAI" });
|
||||
expect(getModelInfo("runway-gen4")).toEqual({ name: "Runway Gen-4", provider: "Runway" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidModelSlug", () => {
|
||||
it("returns true for valid model slugs", () => {
|
||||
expect(isValidModelSlug("gpt-4o")).toBe(true);
|
||||
expect(isValidModelSlug("claude-3-5-sonnet")).toBe(true);
|
||||
expect(isValidModelSlug("gemini-2-5-flash")).toBe(true);
|
||||
expect(isValidModelSlug("grok-3")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for invalid slugs", () => {
|
||||
expect(isValidModelSlug("invalid")).toBe(false);
|
||||
expect(isValidModelSlug("gpt-10")).toBe(false);
|
||||
expect(isValidModelSlug("")).toBe(false);
|
||||
});
|
||||
|
||||
it("validates all defined model slugs", () => {
|
||||
for (const slug of Object.keys(AI_MODELS)) {
|
||||
expect(isValidModelSlug(slug)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getModelsByProvider", () => {
|
||||
it("returns models grouped by provider", () => {
|
||||
const grouped = getModelsByProvider();
|
||||
|
||||
expect(grouped).toHaveProperty("OpenAI");
|
||||
expect(grouped).toHaveProperty("Anthropic");
|
||||
expect(grouped).toHaveProperty("Google");
|
||||
expect(grouped).toHaveProperty("xAI");
|
||||
});
|
||||
|
||||
it("includes all OpenAI models under OpenAI provider", () => {
|
||||
const grouped = getModelsByProvider();
|
||||
const openaiModels = grouped["OpenAI"];
|
||||
|
||||
expect(openaiModels).toBeDefined();
|
||||
expect(openaiModels.some(m => m.slug === "gpt-4o")).toBe(true);
|
||||
expect(openaiModels.some(m => m.slug === "dall-e-3")).toBe(true);
|
||||
expect(openaiModels.some(m => m.slug === "sora 2")).toBe(true);
|
||||
});
|
||||
|
||||
it("includes all Anthropic models under Anthropic provider", () => {
|
||||
const grouped = getModelsByProvider();
|
||||
const anthropicModels = grouped["Anthropic"];
|
||||
|
||||
expect(anthropicModels).toBeDefined();
|
||||
expect(anthropicModels.some(m => m.slug === "claude-4-opus")).toBe(true);
|
||||
expect(anthropicModels.some(m => m.slug === "claude-3-5-sonnet")).toBe(true);
|
||||
});
|
||||
|
||||
it("each model entry has slug and name", () => {
|
||||
const grouped = getModelsByProvider();
|
||||
|
||||
for (const provider of Object.keys(grouped)) {
|
||||
for (const model of grouped[provider]) {
|
||||
expect(model).toHaveProperty("slug");
|
||||
expect(model).toHaveProperty("name");
|
||||
expect(typeof model.slug).toBe("string");
|
||||
expect(typeof model.name).toBe("string");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("covers all models in AI_MODELS", () => {
|
||||
const grouped = getModelsByProvider();
|
||||
let totalModels = 0;
|
||||
|
||||
for (const provider of Object.keys(grouped)) {
|
||||
totalModels += grouped[provider].length;
|
||||
}
|
||||
|
||||
expect(totalModels).toBe(Object.keys(AI_MODELS).length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateBestWithModels", () => {
|
||||
describe("valid inputs", () => {
|
||||
it("accepts empty array", () => {
|
||||
const result = validateBestWithModels([]);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts single valid model", () => {
|
||||
const result = validateBestWithModels(["gpt-4o"]);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts up to 3 valid models", () => {
|
||||
const result = validateBestWithModels(["gpt-4o", "claude-4-opus", "gemini-2-5-pro"]);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid inputs", () => {
|
||||
it("rejects more than 3 models", () => {
|
||||
const result = validateBestWithModels(["gpt-4o", "claude-4-opus", "gemini-2-5-pro", "grok-3"]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("Maximum 3 models allowed");
|
||||
});
|
||||
|
||||
it("rejects unknown model slugs", () => {
|
||||
const result = validateBestWithModels(["gpt-4o", "unknown-model"]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("Unknown model: unknown-model");
|
||||
});
|
||||
|
||||
it("reports multiple errors", () => {
|
||||
const result = validateBestWithModels(["invalid1", "invalid2", "invalid3", "invalid4"]);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.length).toBeGreaterThan(1);
|
||||
expect(result.errors).toContain("Maximum 3 models allowed");
|
||||
expect(result.errors).toContain("Unknown model: invalid1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateBestWithMCP", () => {
|
||||
describe("valid inputs", () => {
|
||||
it("accepts null", () => {
|
||||
const result = validateBestWithMCP(null);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts undefined", () => {
|
||||
const result = validateBestWithMCP(undefined);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts valid config with command only", () => {
|
||||
const result = validateBestWithMCP({ command: "npx @modelcontextprotocol/server-filesystem" });
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts valid config with command and tools", () => {
|
||||
const result = validateBestWithMCP({
|
||||
command: "npx @modelcontextprotocol/server-filesystem",
|
||||
tools: ["read_file", "write_file"]
|
||||
});
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("accepts config with empty tools array", () => {
|
||||
const result = validateBestWithMCP({
|
||||
command: "some-command",
|
||||
tools: []
|
||||
});
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid inputs", () => {
|
||||
it("rejects non-object values", () => {
|
||||
expect(validateBestWithMCP("string").valid).toBe(false);
|
||||
expect(validateBestWithMCP(123).valid).toBe(false);
|
||||
expect(validateBestWithMCP([]).valid).toBe(false);
|
||||
expect(validateBestWithMCP(true).valid).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects missing command", () => {
|
||||
const result = validateBestWithMCP({});
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("MCP config.command is required and must be a string");
|
||||
});
|
||||
|
||||
it("rejects non-string command", () => {
|
||||
const result = validateBestWithMCP({ command: 123 });
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("MCP config.command is required and must be a string");
|
||||
});
|
||||
|
||||
it("rejects non-array tools", () => {
|
||||
const result = validateBestWithMCP({ command: "cmd", tools: "not-array" });
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("MCP config.tools must be an array");
|
||||
});
|
||||
|
||||
it("rejects tools array with non-string elements", () => {
|
||||
const result = validateBestWithMCP({ command: "cmd", tools: ["valid", 123, "also-valid"] });
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain("MCP config.tools must be an array of strings");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles object with extra properties", () => {
|
||||
const result = validateBestWithMCP({
|
||||
command: "cmd",
|
||||
tools: ["tool1"],
|
||||
extraProp: "ignored"
|
||||
});
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it("handles deeply nested invalid tools", () => {
|
||||
const result = validateBestWithMCP({
|
||||
command: "cmd",
|
||||
tools: [["nested"]]
|
||||
});
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user