fix(koreader): accept optional KOSync document metadata (#966)

* fix(koreader): accept optional KOSync document metadata

* fix(koreader): reject null progress metadata
This commit is contained in:
Jadehawk
2026-08-10 19:42:59 -04:00
committed by GitHub
parent 2b215141a9
commit 80223b5e67
3 changed files with 44 additions and 1 deletions
@@ -8,6 +8,29 @@ import { KoreaderSaveProgressDto } from './koreader-progress.dto';
const BASE = { document: 'abc123', percentage: 0.42 };
describe('KoreaderSaveProgressDto', () => {
it('accepts optional document metadata from newer KOSync clients', async () => {
const metadata = { filename: 'book.epub', title: 'Book', authors: 'Author' };
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, metadata });
const errors = await validate(dto, { whitelist: true, forbidNonWhitelisted: true });
expect(errors).toHaveLength(0);
expect(dto.metadata).toEqual(metadata);
});
it('rejects metadata values that are not objects', async () => {
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, metadata: 'book.epub' });
const errors = await validate(dto);
expect(errors).toEqual([expect.objectContaining({ property: 'metadata' })]);
});
it('rejects explicit null metadata', async () => {
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, metadata: null });
const errors = await validate(dto);
expect(errors).toEqual([expect.objectContaining({ property: 'metadata' })]);
});
it('accepts an xpointer string progress from reflowable documents', async () => {
const dto = plainToInstance(KoreaderSaveProgressDto, { ...BASE, progress: '/body/DocFragment[8]/body/p[12]/text().0' });
const errors = await validate(dto);
@@ -1,10 +1,14 @@
import { Transform } from 'class-transformer';
import { IsNumber, IsOptional, IsString, Max, Min } from 'class-validator';
import { IsNumber, IsObject, IsOptional, IsString, Max, Min, ValidateIf } from 'class-validator';
export class KoreaderSaveProgressDto {
@IsString()
document!: string;
@IsObject()
@ValidateIf((_object, value) => value !== undefined)
metadata?: Record<string, unknown>;
@IsNumber()
@Min(0)
@Max(1)
@@ -150,6 +150,22 @@ describe('KOReader progress position routing (e2e)', { timeout: 180_000 }, () =>
expect((await readerProgress(epub.bookFileId)).pageNumber).toBeNull();
});
it('accepts document metadata on progress sync requests', async () => {
const response = await ctx.app.inject({
method: 'PUT',
url: '/api/v1/koreader/syncs/progress',
headers: deviceHeaders(),
payload: {
document: epubHash,
percentage: 0.35,
progress: XPOINTER,
metadata: { filename: 'progress-position-book.epub', title: 'Progress Position Book', authors: 'Test Author' },
},
});
expect(response.statusCode).toBe(200);
});
it('replaces a page saved by the web reader instead of discarding it', async () => {
await saveFromWebReader(comic.bookFileId, { percentage: 12, pageNumber: 34 });
expect((await storedProgress(comic.bookFileId))?.pageNumber).toBe(34);