feat: 为 API 增加文章封面字段支持并添加单元测试

This commit is contained in:
seahi committed 2026-10-07 10:33:47 +08:00
1 parent fdc0af0c10
commit 3a543a7ca5
3 files changed
+203 -7

No files matched your search

+8 -5
View File
@@ -12,7 +12,7 @@ export const GET: RequestHandler = async ({ platform, url }) => {
const { results } = await db
.prepare(
`
SELECT p.id, p.slug, p.title, p.published_at, p.updated_at, c.slug as category_slug, c.name as category_name, s.slug as series_slug, s.name as series_name
SELECT p.id, p.slug, p.title, p.cover, p.published_at, p.updated_at, c.slug as category_slug, c.name as category_name, s.slug as series_slug, s.name as series_name
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN series s ON p.series_id = s.id
@@ -46,10 +46,11 @@ export const POST: RequestHandler = async ({ request, platform }) => {
try {
const data = (await request.json()) as Record<string, unknown>;
const { slug, title, content, category_slug, series_slug, tags } = data as {
const { slug, title, content, cover, category_slug, series_slug, tags } = data as {
slug?: string;
title?: string;
content?: string;
cover?: string | null;
category_slug?: string;
series_slug?: string;
tags?: string[];
@@ -80,14 +81,16 @@ export const POST: RequestHandler = async ({ request, platform }) => {
if (seriesResult) seriesId = seriesResult.id;
}
const postCover = cover ? String(cover).trim() || null : null;
const result = await db
.prepare(
`
INSERT INTO posts (slug, title, content, author_id, category_id, series_id, published_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
INSERT INTO posts (slug, title, content, cover, author_id, category_id, series_id, published_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`
)
.bind(slug, title, content, authorId, categoryId, seriesId)
.bind(slug, title, content, postCover, authorId, categoryId, seriesId)
.run();
if (result.success) {
+9 -2
View File
@@ -11,7 +11,7 @@ export const GET: RequestHandler = async ({ params, platform }) => {
const post = await db
.prepare(
`
SELECT p.id, p.slug, p.title, p.content, p.published_at, p.updated_at, c.slug as category_slug, c.name as category_name, s.slug as series_slug, s.name as series_name
SELECT p.id, p.slug, p.title, p.content, p.cover, p.published_at, p.updated_at, c.slug as category_slug, c.name as category_name, s.slug as series_slug, s.name as series_name
FROM posts p
LEFT JOIN categories c ON p.category_id = c.id
LEFT JOIN series s ON p.series_id = s.id
@@ -54,9 +54,10 @@ export const PUT: RequestHandler = async ({ params, request, platform }) => {
try {
const data = (await request.json()) as Record<string, unknown>;
const { title, content, category_slug, series_slug, tags } = data as {
const { title, content, cover, category_slug, series_slug, tags } = data as {
title?: string;
content?: string;
cover?: string | null;
category_slug?: string;
series_slug?: string;
tags?: string[];
@@ -65,6 +66,7 @@ export const PUT: RequestHandler = async ({ params, request, platform }) => {
if (
!title &&
!content &&
cover === undefined &&
category_slug === undefined &&
series_slug === undefined &&
tags === undefined
@@ -92,6 +94,11 @@ export const PUT: RequestHandler = async ({ params, request, platform }) => {
updates.push('content = ?');
bindings.push(content);
}
if (cover !== undefined) {
updates.push('cover = ?');
const postCover = cover ? String(cover).trim() || null : null;
bindings.push(postCover);
}
if (category_slug !== undefined) {
let categoryId = null;
if (category_slug) {
+186
View File
@@ -0,0 +1,186 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, expect, it, vi } from 'vitest';
import { GET as getPosts, POST as createPost } from './+server';
import { GET as getPostDetail, PUT as updatePost } from './[slug]/+server';
describe('API v1 posts cover support', () => {
it('GET /api/v1/posts includes cover in SQL select query', async () => {
const mockAll = vi.fn().mockResolvedValue({
results: [
{
id: 1,
slug: 'test',
title: 'Test',
cover: 'https://example.com/cover.png',
published_at: '2026-01-01',
updated_at: '2026-01-01'
}
]
});
const mockBind = vi.fn().mockReturnValue({ all: mockAll });
const mockPrepare = vi.fn((sql: string) => {
if (sql.includes('COUNT(*)')) {
return { all: vi.fn().mockResolvedValue({ results: [{ total: 1 }] }) };
}
return { bind: mockBind };
});
const event = {
platform: {
env: {
DB: { prepare: mockPrepare }
}
},
url: new URL('https://example.com/api/v1/posts?limit=10&offset=0')
} as unknown as Parameters<typeof getPosts>[0];
const response = await getPosts(event);
const json = (await response.json()) as any;
expect(mockPrepare.mock.calls[0][0]).toContain('p.cover');
expect(json.data[0].cover).toBe('https://example.com/cover.png');
});
it('POST /api/v1/posts saves cover into database', async () => {
const boundArgs: unknown[][] = [];
const mockRun = vi.fn().mockResolvedValue({ success: true });
const mockBind = vi.fn((...args: unknown[]) => {
boundArgs.push(args);
return { run: mockRun, first: vi.fn().mockResolvedValue({ id: 1 }) };
});
const mockPrepare = vi.fn((sql: string) => {
void sql;
return {
bind: mockBind,
run: mockRun,
first: vi.fn().mockResolvedValue({ id: 1 })
};
});
const event = {
platform: {
env: {
DB: { prepare: mockPrepare }
}
},
request: new Request('https://example.com/api/v1/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
slug: 'new-post',
title: 'New Post',
content: 'Hello world',
cover: ' https://example.com/banner.jpg '
})
})
} as unknown as Parameters<typeof createPost>[0];
const response = await createPost(event);
expect(response.status).toBe(200);
const insertCall = mockPrepare.mock.calls.find((call) =>
(call[0] as string).includes('INSERT INTO posts')
);
expect(insertCall).toBeDefined();
expect(insertCall![0]).toContain('cover');
expect(boundArgs[0]).toContain('https://example.com/banner.jpg');
});
it('GET /api/v1/posts/[slug] queries and returns cover', async () => {
const mockFirst = vi.fn().mockResolvedValue({
id: 1,
slug: 'my-post',
title: 'My Post',
content: 'Body',
cover: 'https://example.com/pic.jpg',
published_at: '2026-01-01',
updated_at: '2026-01-01'
});
const mockPrepare = vi.fn((sql: string) => {
if (sql.includes('SELECT t.name')) {
return {
bind: vi.fn().mockReturnValue({ all: vi.fn().mockResolvedValue({ results: [] }) })
};
}
return { bind: vi.fn().mockReturnValue({ first: mockFirst }) };
});
const event = {
params: { slug: 'my-post' },
platform: {
env: {
DB: { prepare: mockPrepare }
}
}
} as unknown as Parameters<typeof getPostDetail>[0];
const response = await getPostDetail(event);
const json = (await response.json()) as any;
expect(mockPrepare.mock.calls[0][0]).toContain('p.cover');
expect(json.data.cover).toBe('https://example.com/pic.jpg');
});
it('PUT /api/v1/posts/[slug] updates or clears cover', async () => {
const boundArgs: unknown[][] = [];
const mockRun = vi.fn().mockResolvedValue({ success: true });
const mockBind = vi.fn((...args: unknown[]) => {
boundArgs.push(args);
return { run: mockRun };
});
const mockPrepare = vi.fn((sql: string) => {
if (sql.includes('SELECT id FROM posts')) {
return { bind: vi.fn().mockReturnValue({ first: vi.fn().mockResolvedValue({ id: 1 }) }) };
}
return { bind: mockBind };
});
// 1. Update cover with new value
const updateEvent = {
params: { slug: 'my-post' },
platform: {
env: {
DB: { prepare: mockPrepare }
}
},
request: new Request('https://example.com/api/v1/posts/my-post', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cover: 'https://example.com/new-cover.jpg'
})
})
} as unknown as Parameters<typeof updatePost>[0];
const res1 = await updatePost(updateEvent);
expect(res1.status).toBe(200);
const updateCall1 = mockPrepare.mock.calls.find((call) =>
(call[0] as string).includes('UPDATE posts')
);
expect(updateCall1![0]).toContain('cover = ?');
expect(boundArgs[0]).toContain('https://example.com/new-cover.jpg');
// 2. Clear cover by passing null or empty string
boundArgs.length = 0;
mockPrepare.mockClear();
const clearEvent = {
params: { slug: 'my-post' },
platform: {
env: {
DB: { prepare: mockPrepare }
}
},
request: new Request('https://example.com/api/v1/posts/my-post', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
cover: ''
})
})
} as unknown as Parameters<typeof updatePost>[0];
const res2 = await updatePost(clearEvent);
expect(res2.status).toBe(200);
expect(boundArgs[0]).toContain(null);
});
});