feat: edit existing manga metadata (0.31.0)
Adds PUT /mangas/:id/cover (multipart) and DELETE /mangas/:id/cover so covers can be replaced or cleared after creation, and wires a dedicated /manga/[id]/edit SvelteKit route that combines the existing PATCH with the new cover endpoints. Cover PUT cleans up the old blob when the extension changes, swallowing StorageError::NotFound so a manually-gone file doesn't surface as a 404 to the client. Edit link on the manga detail page is gated on session.user, matching the auth posture of the underlying handlers. Also pins the local-dev port story via loadEnv() in vite.config.ts so VITE_PORT / BACKEND_URL from a (gitignored) .env keep the dev URL stable across runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ import {
|
||||
createManga,
|
||||
getManga,
|
||||
updateManga,
|
||||
updateMangaCover,
|
||||
deleteMangaCover,
|
||||
attachTag,
|
||||
detachTag
|
||||
} from './mangas';
|
||||
@@ -184,6 +186,49 @@ describe('mangas api client', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('updateMangaCover PUTs multipart with the cover blob', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok(detailFixture({ cover_image_path: 'mangas/b1/cover.png' }))
|
||||
);
|
||||
const cover = new Blob([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], { type: 'image/png' });
|
||||
const updated = await updateMangaCover('b1', cover);
|
||||
expect(updated.cover_image_path).toBe('mangas/b1/cover.png');
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/mangas\/b1\/cover$/);
|
||||
const init = fetchSpy.mock.calls[0][1] as RequestInit;
|
||||
expect(init.method).toBe('PUT');
|
||||
expect(init.body).toBeInstanceOf(FormData);
|
||||
const form = init.body as FormData;
|
||||
expect(form.get('cover')).toBeInstanceOf(Blob);
|
||||
// Boundary is filled in by the browser when body is FormData.
|
||||
expect(init.headers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('updateMangaCover throws ApiError on payload_too_large', async () => {
|
||||
fetchSpy.mockResolvedValue(
|
||||
envelope(413, 'payload_too_large', 'cover exceeds size cap')
|
||||
);
|
||||
const cover = new Blob([new Uint8Array(1)]);
|
||||
await expect(updateMangaCover('b1', cover)).rejects.toMatchObject({
|
||||
name: 'ApiError',
|
||||
status: 413,
|
||||
code: 'payload_too_large'
|
||||
});
|
||||
});
|
||||
|
||||
it('deleteMangaCover DELETEs and returns the refreshed detail with null path', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok(detailFixture({ cover_image_path: null }))
|
||||
);
|
||||
const updated = await deleteMangaCover('b1');
|
||||
expect(updated.cover_image_path).toBeNull();
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/mangas\/b1\/cover$/);
|
||||
const init = fetchSpy.mock.calls[0][1] as RequestInit;
|
||||
expect(init.method).toBe('DELETE');
|
||||
expect(init.body).toBeUndefined();
|
||||
});
|
||||
|
||||
it('attachTag POSTs the name and returns the TagRef', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(
|
||||
ok({ id: 't9', name: 'Dark Fantasy', added_by: 'u1' }, 201)
|
||||
|
||||
Reference in New Issue
Block a user