Files
bookorbit/server/src/modules/book/reading-progress.controller.test.ts
T
neon 9e49f496f1 feat(reader): add grouped reading progress routes
Add GET and PUT /reading-progress and DELETE /reading-progress/:fileId: positions grouped by
kind, up to 25 files saved in one request, and an optional editedAt so the server keeps the newer
of two saves. Move the web reader and book page to them, and list reading-progress in
SERVER_FEATURES for the iOS and Android apps, which adopt the routes in the same release.
2026-10-22 17:28:07 +00:00

57 lines
2.2 KiB
TypeScript

import { BadRequestException } from '@nestjs/common';
import { describe, expect, it, vi } from 'vitest';
import { PERMISSION_KEY } from '../../common/decorators/require-permission.decorator';
import type { RequestUser } from '../../common/types/request-user';
import { ReadingProgressController } from './reading-progress.controller';
const user = { id: 7 } as RequestUser;
function makeController() {
const progress = {
forFile: vi.fn().mockResolvedValue({ items: [] }),
forBook: vi.fn().mockResolvedValue({ items: [] }),
save: vi.fn().mockResolvedValue({ results: [] }),
clear: vi.fn().mockResolvedValue(undefined),
};
return { controller: new ReadingProgressController(progress as never), progress };
}
describe('ReadingProgressController', () => {
it('reads one file or one book', async () => {
const { controller, progress } = makeController();
await controller.list({ fileId: 64 }, user);
await controller.list({ bookId: 60 }, user);
expect(progress.forFile).toHaveBeenCalledWith(64, user);
expect(progress.forBook).toHaveBeenCalledWith(60, user);
});
it('rejects a read that names both a file and a book, or neither', () => {
const { controller } = makeController();
expect(() => controller.list({ fileId: 64, bookId: 60 }, user)).toThrow(BadRequestException);
expect(() => controller.list({}, user)).toThrow(BadRequestException);
});
it('passes the writes and the caller through', async () => {
const { controller, progress } = makeController();
const writes = [{ fileId: 64, percentage: 8 }];
await controller.save({ writes }, user);
expect(progress.save).toHaveBeenCalledWith(writes, user);
});
it('clears one file for the caller', async () => {
const { controller, progress } = makeController();
await controller.clear(64, user);
expect(progress.clear).toHaveBeenCalledWith(64, user);
});
it('requires no permission, since every route touches only the caller position', () => {
for (const route of [
ReadingProgressController.prototype.list,
ReadingProgressController.prototype.save,
ReadingProgressController.prototype.clear,
]) {
expect(Reflect.getMetadata(PERMISSION_KEY, route)).toBeUndefined();
}
});
});