mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
fix(kobo): stop a replacement with no earlier copy from blocking delivery
A replacement prepared before the device ever received the book had no earlier delivery to fall back to, so every sync failed the book. It now stops standing for the book and the book gets an ordinary first offer. Cancelling a replacement the device has not fetched now clears it, hands the book back to the earlier delivery, and re-announces it. Update the download E2E suite for the stale-offer replacement from #1816. Ref #1786
This commit is contained in:
@@ -222,7 +222,6 @@ describe('Kobo download identity in KOReader (e2e)', { timeout: 180_000 }, () =>
|
||||
expect(Date.now() - duplicateStart).toBeLessThan(1000);
|
||||
vi.spyOn(ctx.app.get(KepubConversionService), 'getKepubPath').mockResolvedValue(path);
|
||||
const startedAt = Date.now();
|
||||
expect((await download()).statusCode).toBe(409);
|
||||
const response = await download(freshDevice.token);
|
||||
const durationMs = Date.now() - startedAt;
|
||||
expect(response.statusCode).toBe(200);
|
||||
@@ -537,4 +536,31 @@ describe('Kobo download identity in KOReader (e2e)', { timeout: 180_000 }, () =>
|
||||
expect(after.libraryVersion).not.toBe(before);
|
||||
expect(after.matches).toEqual([]);
|
||||
});
|
||||
|
||||
it('replaces an offer its source can no longer rebuild instead of failing every download of the book', async () => {
|
||||
const created = await ctx.app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/v1/kobo/devices',
|
||||
headers: authHeader(user.accessToken),
|
||||
payload: { name: 'Stale offer device' },
|
||||
});
|
||||
const device = created.json() as { id: number; token: string };
|
||||
const deliveries = ctx.app.get(KoboDeliveryService);
|
||||
const conversion = vi.spyOn(ctx.app.get(KepubConversionService), 'getKepubPath').mockResolvedValue(downloadPath);
|
||||
const stale = await deliveries.offer(user.userId, device.id, source.bookId);
|
||||
const rebuilt = await createEpubFixture(ctx.fixture.rootPath, 'rebuilt.epub', { title: 'Rebuilt conversion', uid: `urn:uuid:${randomUUID()}` });
|
||||
conversion.mockResolvedValue(rebuilt);
|
||||
try {
|
||||
const response = await download(device.token);
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.rawPayload).toEqual(await readFile(rebuilt));
|
||||
const fresh = await deliveries.current(user.userId, device.id, source.bookId);
|
||||
expect(fresh!.id).not.toBe(stale.id);
|
||||
expect(fresh!.previousId).toBeNull();
|
||||
expect((await deliveries.byId(user.userId, device.id, stale.id))!.current).toBe(false);
|
||||
expect((await download(device.token)).rawPayload).toEqual(await readFile(rebuilt));
|
||||
} finally {
|
||||
conversion.mockResolvedValue(downloadPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,6 +413,32 @@ describe('Read-aloud delivery contracts (e2e)', { timeout: 180_000 }, () => {
|
||||
expect((await action('cancel')).json().state).toBe('user_confirmed');
|
||||
});
|
||||
|
||||
it('delivers a book whose replacement was prepared before any copy, and an unfetched cancel hands back the earlier copy', async () => {
|
||||
const device = (await request('POST', '/api/v1/kobo/devices', { name: 'Replacement before delivery' })).json();
|
||||
await request('PATCH', '/api/v1/kobo/settings', { readAloudDelivery: 'original' });
|
||||
const base = `/api/v1/kobo/devices/${device.id}`;
|
||||
const download = () => ctx.app.inject({ method: 'GET', url: `/api/v1/kobo/${device.token}/v1/books/${book.bookId}/download` });
|
||||
|
||||
const early = (await request('POST', `${base}/books/${book.bookId}/replacement`, { variant: 'audioless_epub' })).json();
|
||||
expect(early).toMatchObject({ state: 'prepared', previousId: null });
|
||||
const first = await download();
|
||||
expect(first.statusCode).toBe(200);
|
||||
expect(first.rawPayload).toEqual(sourceBytes);
|
||||
const delivered = (await request('GET', `${base}/books/${book.bookId}/delivery`)).json();
|
||||
expect(delivered.id).not.toBe(early.id);
|
||||
expect(delivered.options.replacement).toBe(false);
|
||||
await waitForCondition(async () => {
|
||||
const row = await ctx.db.query.koboDeviceDeliveries.findFirst({ where: eq(schema.koboDeviceDeliveries.id, delivered.id) });
|
||||
expect(row?.state).toBe('transfer_observed');
|
||||
});
|
||||
|
||||
const pending = (await request('POST', `${base}/books/${book.bookId}/replacement`, { variant: 'audioless_epub' })).json();
|
||||
expect((await request('POST', `${base}/replacements/${pending.id}/offer`)).json().state).toBe('offered');
|
||||
expect((await request('POST', `${base}/replacements/${pending.id}/cancel`)).json()).toMatchObject({ state: 'cancelled', current: false });
|
||||
expect((await request('GET', `${base}/books/${book.bookId}/delivery`)).json()).toMatchObject({ id: delivered.id, state: 'transfer_observed' });
|
||||
expect((await download()).rawPayload).toEqual(sourceBytes);
|
||||
});
|
||||
|
||||
describe('Storyteller progress-reset regressions over live HTTP', () => {
|
||||
let origin: string;
|
||||
const cfi = 'epubcfi(/6/2!/4/2/2/1:3)';
|
||||
|
||||
Reference in New Issue
Block a user