feat: 为 API 增加文章封面字段支持并添加单元测试
This commit is contained in:
1 parent
fdc0af0c10
commit
3a543a7ca5
3 files changed
+203
-7
No files matched your search
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user