fix(kobo): stop proxying delete requests for bookorbit collection tags

The DELETE handler for a synced BookOrbit collection tag (col-*) was
missing, so requests fell through to the catch-all proxy and were
forwarded to Kobo's real cloud API. That API rejects the unknown
locally-generated tag id with a 400, so the device never receives a
success ack and endlessly retries the deletion during two-way sync,
keeping sync from ever completing.

Add a dedicated DELETE route that acknowledges col-* tag ids locally,
mirroring the existing add/remove items handlers, and proxies through
unrecognized tag ids as before.

Closes #536
This commit is contained in:
neon
2026-07-06 16:50:02 +00:00
parent 000c380f4f
commit 55967fe977
2 changed files with 39 additions and 0 deletions
@@ -272,6 +272,38 @@ describe('KoboSyncController', () => {
expect(reply.send).not.toHaveBeenCalled();
});
it('acknowledges delete-tag requests for synced BookOrbit collection tags', async () => {
const req = { method: 'DELETE', url: '/api/v1/kobo/token/v1/library/tags/col-1' };
const reply = makeReply();
await controller.deleteTag('col-1', { deviceToken: 'token-1' } as never, req as never, reply as never);
expect(reply.status).toHaveBeenCalledWith(HttpStatus.OK);
expect(reply.send).toHaveBeenCalledWith({ RequestResult: 'Success' });
expect(proxyService.forward).not.toHaveBeenCalled();
});
it('proxies delete-tag requests for non-BookOrbit Kobo tags', async () => {
const req = { method: 'DELETE', url: '/api/v1/kobo/token/v1/library/tags/kobo-tag' };
const reply = makeReply();
await controller.deleteTag('kobo-tag', { deviceToken: 'token-1' } as never, req as never, reply as never);
expect(proxyService.forward).toHaveBeenCalledWith(req, reply, 'token-1');
expect(reply.send).not.toHaveBeenCalled();
});
it('does not proxy delete-tag requests for BookOrbit tag ids using a different device token', async () => {
const req = { method: 'DELETE', url: '/api/v1/kobo/other-token/v1/library/tags/col-42' };
const reply = makeReply();
await controller.deleteTag('col-42', { deviceToken: 'other-token' } as never, req as never, reply as never);
expect(reply.status).toHaveBeenCalledWith(HttpStatus.OK);
expect(reply.send).toHaveBeenCalledWith({ RequestResult: 'Success' });
expect(proxyService.forward).not.toHaveBeenCalled();
});
it('proxies metadata/state/delete for non-numeric book ids', async () => {
const req = { method: 'GET', url: '/api/v1/kobo/token/v1/library/abc/metadata' };
const reply = makeReply();
@@ -165,6 +165,13 @@ export class KoboSyncController {
reply.status(HttpStatus.OK).send({ RequestResult: 'Success' });
}
@Delete('v1/library/tags/:tagId')
@HttpCode(HttpStatus.OK)
async deleteTag(@Param('tagId') tagId: string, @KoboDevice() device: KoboDeviceContext, @Req() req: FastifyRequest, @Res() reply: FastifyReply) {
if (!tagId.startsWith('col-')) return this.proxyService.forward(req, reply, device.deviceToken);
reply.status(HttpStatus.OK).send({ RequestResult: 'Success' });
}
@Get('v1/library/:bookId/metadata')
async getBookMetadata(
@Param('bookId') bookId: string,