Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
312 lines
9.5 KiB
TypeScript
312 lines
9.5 KiB
TypeScript
import type { Kysely } from "kysely";
|
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
|
|
import { BylineRepository } from "../../../src/database/repositories/byline.js";
|
|
import { ContentRepository } from "../../../src/database/repositories/content.js";
|
|
import { UserRepository } from "../../../src/database/repositories/user.js";
|
|
import type { Database } from "../../../src/database/types.js";
|
|
import { SQL_BATCH_SIZE } from "../../../src/utils/chunks.js";
|
|
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../../utils/test-db.js";
|
|
|
|
// Mock the loader's getDb to return our test database
|
|
vi.mock("../../../src/loader.js", () => ({
|
|
getDb: vi.fn(),
|
|
}));
|
|
|
|
import {
|
|
getByline,
|
|
getBylineBySlug,
|
|
getEntryBylines,
|
|
getBylinesForEntries,
|
|
} from "../../../src/bylines/index.js";
|
|
import { getDb } from "../../../src/loader.js";
|
|
|
|
describe("Byline query functions", () => {
|
|
let db: Kysely<Database>;
|
|
let bylineRepo: BylineRepository;
|
|
let contentRepo: ContentRepository;
|
|
|
|
beforeEach(async () => {
|
|
db = await setupTestDatabaseWithCollections();
|
|
bylineRepo = new BylineRepository(db);
|
|
contentRepo = new ContentRepository(db);
|
|
vi.mocked(getDb).mockResolvedValue(db);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await teardownTestDatabase(db);
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("getByline", () => {
|
|
it("returns a byline by ID", async () => {
|
|
const created = await bylineRepo.create({
|
|
slug: "jane-doe",
|
|
displayName: "Jane Doe",
|
|
});
|
|
|
|
const result = await getByline(created.id);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.id).toBe(created.id);
|
|
expect(result?.displayName).toBe("Jane Doe");
|
|
expect(result?.slug).toBe("jane-doe");
|
|
});
|
|
|
|
it("returns null for non-existent ID", async () => {
|
|
const result = await getByline("non-existent");
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getBylineBySlug", () => {
|
|
it("returns a byline by slug", async () => {
|
|
await bylineRepo.create({
|
|
slug: "john-smith",
|
|
displayName: "John Smith",
|
|
});
|
|
|
|
const result = await getBylineBySlug("john-smith");
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.displayName).toBe("John Smith");
|
|
});
|
|
|
|
it("returns null for non-existent slug", async () => {
|
|
const result = await getBylineBySlug("nobody");
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getEntryBylines", () => {
|
|
it("returns explicit byline credits for an entry", async () => {
|
|
const lead = await bylineRepo.create({
|
|
slug: "lead-author",
|
|
displayName: "Lead Author",
|
|
});
|
|
const editor = await bylineRepo.create({
|
|
slug: "editor",
|
|
displayName: "Editor",
|
|
});
|
|
|
|
const post = await contentRepo.create({
|
|
type: "post",
|
|
slug: "my-post",
|
|
data: { title: "My Post" },
|
|
});
|
|
|
|
await bylineRepo.setContentBylines("post", post.id, [
|
|
{ bylineId: lead.id },
|
|
{ bylineId: editor.id, roleLabel: "Contributing Editor" },
|
|
]);
|
|
|
|
const bylines = await getEntryBylines("post", post.id);
|
|
|
|
expect(bylines).toHaveLength(2);
|
|
expect(bylines[0]?.byline.displayName).toBe("Lead Author");
|
|
expect(bylines[0]?.sortOrder).toBe(0);
|
|
expect(bylines[0]?.source).toBe("explicit");
|
|
expect(bylines[1]?.byline.displayName).toBe("Editor");
|
|
expect(bylines[1]?.roleLabel).toBe("Contributing Editor");
|
|
expect(bylines[1]?.source).toBe("explicit");
|
|
});
|
|
|
|
it("falls back to user-linked byline when no explicit credits", async () => {
|
|
// Create a user
|
|
const userRepo = new UserRepository(db);
|
|
const user = await userRepo.create({
|
|
email: "author@example.com",
|
|
displayName: "Author User",
|
|
role: "editor",
|
|
});
|
|
|
|
// Create a byline linked to the user
|
|
await bylineRepo.create({
|
|
slug: "author-user",
|
|
displayName: "Author User",
|
|
userId: user.id,
|
|
});
|
|
|
|
// Create a post with this user as author, no explicit bylines
|
|
const post = await contentRepo.create({
|
|
type: "post",
|
|
slug: "authored-post",
|
|
data: { title: "Authored Post" },
|
|
authorId: user.id,
|
|
});
|
|
|
|
const bylines = await getEntryBylines("post", post.id);
|
|
|
|
expect(bylines).toHaveLength(1);
|
|
expect(bylines[0]?.byline.displayName).toBe("Author User");
|
|
expect(bylines[0]?.source).toBe("inferred");
|
|
expect(bylines[0]?.roleLabel).toBeNull();
|
|
});
|
|
|
|
it("returns empty array when no bylines and no author fallback", async () => {
|
|
const post = await contentRepo.create({
|
|
type: "post",
|
|
slug: "no-author-post",
|
|
data: { title: "No Author" },
|
|
});
|
|
|
|
const bylines = await getEntryBylines("post", post.id);
|
|
expect(bylines).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("getBylinesForEntries", () => {
|
|
it("batch-fetches byline credits for multiple entries", async () => {
|
|
const author1 = await bylineRepo.create({
|
|
slug: "author-one",
|
|
displayName: "Author One",
|
|
});
|
|
const author2 = await bylineRepo.create({
|
|
slug: "author-two",
|
|
displayName: "Author Two",
|
|
});
|
|
|
|
const post1 = await contentRepo.create({
|
|
type: "post",
|
|
slug: "post-1",
|
|
data: { title: "Post 1" },
|
|
});
|
|
const post2 = await contentRepo.create({
|
|
type: "post",
|
|
slug: "post-2",
|
|
data: { title: "Post 2" },
|
|
});
|
|
const post3 = await contentRepo.create({
|
|
type: "post",
|
|
slug: "post-3",
|
|
data: { title: "Post 3" },
|
|
});
|
|
|
|
await bylineRepo.setContentBylines("post", post1.id, [{ bylineId: author1.id }]);
|
|
await bylineRepo.setContentBylines("post", post2.id, [
|
|
{ bylineId: author1.id },
|
|
{ bylineId: author2.id, roleLabel: "Contributor" },
|
|
]);
|
|
// post3 has no bylines
|
|
|
|
const result = await getBylinesForEntries(
|
|
"post",
|
|
[post1, post2, post3].map((p) => ({ id: p.id, authorId: p.authorId })),
|
|
);
|
|
|
|
expect(result.get(post1.id)).toHaveLength(1);
|
|
expect(result.get(post1.id)?.[0]?.byline.displayName).toBe("Author One");
|
|
expect(result.get(post1.id)?.[0]?.source).toBe("explicit");
|
|
|
|
expect(result.get(post2.id)).toHaveLength(2);
|
|
expect(result.get(post2.id)?.[0]?.byline.displayName).toBe("Author One");
|
|
expect(result.get(post2.id)?.[1]?.byline.displayName).toBe("Author Two");
|
|
expect(result.get(post2.id)?.[1]?.roleLabel).toBe("Contributor");
|
|
|
|
expect(result.get(post3.id)).toHaveLength(0);
|
|
});
|
|
|
|
it("returns inferred bylines for entries without explicit credits", async () => {
|
|
const userRepo = new UserRepository(db);
|
|
const user = await userRepo.create({
|
|
email: "batch-author@example.com",
|
|
displayName: "Batch Author",
|
|
role: "editor",
|
|
});
|
|
|
|
await bylineRepo.create({
|
|
slug: "batch-author",
|
|
displayName: "Batch Author",
|
|
userId: user.id,
|
|
});
|
|
|
|
const post = await contentRepo.create({
|
|
type: "post",
|
|
slug: "batch-post",
|
|
data: { title: "Batch Post" },
|
|
authorId: user.id,
|
|
});
|
|
|
|
const result = await getBylinesForEntries("post", [{ id: post.id, authorId: post.authorId }]);
|
|
|
|
expect(result.get(post.id)).toHaveLength(1);
|
|
expect(result.get(post.id)?.[0]?.source).toBe("inferred");
|
|
expect(result.get(post.id)?.[0]?.byline.displayName).toBe("Batch Author");
|
|
});
|
|
|
|
it("handles batches larger than SQL_BATCH_SIZE across explicit and inferred bylines", async () => {
|
|
const userRepo = new UserRepository(db);
|
|
const explicitByline = await bylineRepo.create({
|
|
slug: "large-batch-explicit",
|
|
displayName: "Large Batch Explicit",
|
|
});
|
|
|
|
const explicitPost1 = await contentRepo.create({
|
|
type: "post",
|
|
slug: "large-batch-explicit-1",
|
|
data: { title: "Large Batch Explicit 1" },
|
|
});
|
|
await bylineRepo.setContentBylines("post", explicitPost1.id, [
|
|
{ bylineId: explicitByline.id },
|
|
]);
|
|
|
|
const inferredPosts: { id: string; authorId: string | null }[] = [];
|
|
for (let i = 0; i < SQL_BATCH_SIZE + 2; i++) {
|
|
const user = await userRepo.create({
|
|
email: `large-batch-${i}@example.com`,
|
|
displayName: `Large Batch ${i}`,
|
|
role: "editor",
|
|
});
|
|
|
|
await bylineRepo.create({
|
|
slug: `large-batch-${i}`,
|
|
displayName: `Large Batch ${i}`,
|
|
userId: user.id,
|
|
});
|
|
|
|
const post = await contentRepo.create({
|
|
type: "post",
|
|
slug: `large-batch-post-${i}`,
|
|
data: { title: `Large Batch Post ${i}` },
|
|
authorId: user.id,
|
|
});
|
|
inferredPosts.push({ id: post.id, authorId: post.authorId });
|
|
}
|
|
|
|
const explicitPost2 = await contentRepo.create({
|
|
type: "post",
|
|
slug: "large-batch-explicit-2",
|
|
data: { title: "Large Batch Explicit 2" },
|
|
});
|
|
await bylineRepo.setContentBylines("post", explicitPost2.id, [
|
|
{ bylineId: explicitByline.id },
|
|
]);
|
|
|
|
const inferredPostIds = inferredPosts.map((p) => p.id);
|
|
const entries = [
|
|
{ id: explicitPost1.id, authorId: explicitPost1.authorId },
|
|
...inferredPosts,
|
|
{ id: explicitPost2.id, authorId: explicitPost2.authorId },
|
|
];
|
|
const result = await getBylinesForEntries("post", entries);
|
|
|
|
expect(result.size).toBe(entries.length);
|
|
expect(result.get(explicitPost1.id)?.[0]?.source).toBe("explicit");
|
|
expect(result.get(explicitPost1.id)?.[0]?.byline.displayName).toBe("Large Batch Explicit");
|
|
expect(result.get(explicitPost2.id)?.[0]?.source).toBe("explicit");
|
|
expect(result.get(explicitPost2.id)?.[0]?.byline.displayName).toBe("Large Batch Explicit");
|
|
expect(result.get(inferredPostIds[0]!)?.[0]?.source).toBe("inferred");
|
|
expect(result.get(inferredPostIds[0]!)?.[0]?.byline.displayName).toBe("Large Batch 0");
|
|
expect(result.get(inferredPostIds[SQL_BATCH_SIZE + 1]!)?.[0]?.source).toBe("inferred");
|
|
expect(result.get(inferredPostIds[SQL_BATCH_SIZE + 1]!)?.[0]?.byline.displayName).toBe(
|
|
`Large Batch ${SQL_BATCH_SIZE + 1}`,
|
|
);
|
|
});
|
|
|
|
it("returns empty map for empty input", async () => {
|
|
const result = await getBylinesForEntries("post", []);
|
|
expect(result.size).toBe(0);
|
|
});
|
|
});
|
|
});
|