ci: add CI workflow and tests for admin categories and prompts

This commit is contained in:
Fatih Kadir Akın
2026-02-03 12:46:35 +03:00
parent 409152d6ab
commit 72fb2c1662
19 changed files with 4554 additions and 0 deletions

35
.github/workflows/ci.yml vendored Normal file
View 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

View 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,
},
});
});
});

View 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" },
})
);
});
});

View 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",
},
});
});
});

View 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" },
})
);
});
});

View 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,
},
});
});
});

View 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) },
});
});
});

View 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
});
});
});

View 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 });
});
});

View 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);
});
});

View 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,
},
});
});
});

View 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,
},
});
});
});

View 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 },
});
});
});

View 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 },
});
});
});

View 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");
});
});

View 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");
});
});
});

View 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");
});
});

View 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");
});
});
});

View 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);
});
});
});