mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
209 lines
7.7 KiB
TypeScript
209 lines
7.7 KiB
TypeScript
vi.mock('drizzle-orm', () => ({
|
|
and: vi.fn((...clauses: unknown[]) => ({ op: 'and', clauses })),
|
|
asc: vi.fn((value: unknown) => ({ op: 'asc', value })),
|
|
count: vi.fn(() => ({ op: 'count' })),
|
|
desc: vi.fn((value: unknown) => ({ op: 'desc', value })),
|
|
eq: vi.fn((left: unknown, right: unknown) => ({ op: 'eq', left, right })),
|
|
gt: vi.fn((left: unknown, right: unknown) => ({ op: 'gt', left, right })),
|
|
ilike: vi.fn((left: unknown, right: unknown) => ({ op: 'ilike', left, right })),
|
|
inArray: vi.fn((left: unknown, right: unknown[]) => ({ op: 'inArray', left, right })),
|
|
isNull: vi.fn((value: unknown) => ({ op: 'isNull', value })),
|
|
notInArray: vi.fn((left: unknown, right: unknown[]) => ({ op: 'notInArray', left, right })),
|
|
or: vi.fn((...clauses: unknown[]) => ({ op: 'or', clauses })),
|
|
sql: Object.assign(
|
|
vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ op: 'sql', text: strings.join(''), values })),
|
|
{
|
|
join: vi.fn((chunks: unknown[], separator: unknown) => ({ op: 'sql.join', chunks, separator })),
|
|
},
|
|
),
|
|
sum: vi.fn((value: unknown) => ({ op: 'sum', value })),
|
|
}));
|
|
|
|
import { BookBucketRepository } from './book-bucket.repository';
|
|
|
|
function makeDb() {
|
|
const selectBuilder = {
|
|
from: vi.fn(),
|
|
where: vi.fn(),
|
|
orderBy: vi.fn(),
|
|
limit: vi.fn(),
|
|
offset: vi.fn(),
|
|
groupBy: vi.fn(),
|
|
};
|
|
selectBuilder.from.mockReturnValue(selectBuilder);
|
|
selectBuilder.where.mockReturnValue(selectBuilder);
|
|
selectBuilder.orderBy.mockReturnValue(selectBuilder);
|
|
selectBuilder.offset.mockResolvedValue([]);
|
|
selectBuilder.limit.mockReturnValue(selectBuilder);
|
|
selectBuilder.groupBy.mockResolvedValue([]);
|
|
|
|
const updateBuilder = {
|
|
set: vi.fn(),
|
|
where: vi.fn(),
|
|
returning: vi.fn(),
|
|
};
|
|
updateBuilder.set.mockReturnValue(updateBuilder);
|
|
updateBuilder.where.mockReturnValue(updateBuilder);
|
|
updateBuilder.returning.mockResolvedValue([]);
|
|
|
|
const deleteBuilder = {
|
|
where: vi.fn(),
|
|
returning: vi.fn(),
|
|
};
|
|
deleteBuilder.where.mockReturnValue(deleteBuilder);
|
|
deleteBuilder.returning.mockResolvedValue([]);
|
|
|
|
const insertBuilder = {
|
|
values: vi.fn(),
|
|
returning: vi.fn(),
|
|
};
|
|
insertBuilder.values.mockReturnValue(insertBuilder);
|
|
insertBuilder.returning.mockResolvedValue([]);
|
|
|
|
return {
|
|
db: {
|
|
select: vi.fn().mockReturnValue(selectBuilder),
|
|
insert: vi.fn().mockReturnValue(insertBuilder),
|
|
update: vi.fn().mockReturnValue(updateBuilder),
|
|
delete: vi.fn().mockReturnValue(deleteBuilder),
|
|
},
|
|
selectBuilder,
|
|
insertBuilder,
|
|
updateBuilder,
|
|
deleteBuilder,
|
|
};
|
|
}
|
|
|
|
describe('BookBucketRepository', () => {
|
|
it('findByIds/deleteByIds/setTargetsByIds short-circuit empty selections', async () => {
|
|
const { db } = makeDb();
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.findByIds([])).resolves.toEqual([]);
|
|
await expect(repo.deleteByIds([])).resolves.toBeUndefined();
|
|
await expect(repo.setTargetsByIds([], 1, 2)).resolves.toBe(0);
|
|
|
|
expect(db.select).not.toHaveBeenCalled();
|
|
expect(db.delete).not.toHaveBeenCalled();
|
|
expect(db.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('countsByStatus maps queue statuses and computes total', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.groupBy.mockResolvedValue([
|
|
{ status: 'pending', cnt: '3' },
|
|
{ status: 'ready', cnt: '5' },
|
|
{ status: 'error', cnt: '2' },
|
|
{ status: 'extracting', cnt: '1' },
|
|
]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.countsByStatus()).resolves.toEqual({
|
|
pending: 3,
|
|
ready: 5,
|
|
error: 2,
|
|
total: 11,
|
|
});
|
|
});
|
|
|
|
it('getStatistics normalizes null formats and aggregates total size', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.groupBy.mockResolvedValue([
|
|
{ format: 'epub', cnt: '2', totalSize: '2000' },
|
|
{ format: null, cnt: '1', totalSize: null },
|
|
]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.getStatistics()).resolves.toEqual({
|
|
totalSizeBytes: 2000,
|
|
byFormat: [
|
|
{ format: 'epub', count: 2, sizeBytes: 2000 },
|
|
{ format: 'unknown', count: 1, sizeBytes: 0 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('findAllIds returns id list from filtered selection query', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.where.mockResolvedValue([{ id: 4 }, { id: 7 }]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.findAllIds([1], 'pending', 'dune')).resolves.toEqual([4, 7]);
|
|
});
|
|
|
|
it('findSelectionBatch applies deterministic ordering and limit', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.limit.mockResolvedValue([{ id: 11 }, { id: 12 }]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(
|
|
repo.findSelectionBatch({ limit: 2, afterId: 10, excludedIds: [3], status: 'error', search: 'foo', userId: 1, isSuperuser: false }),
|
|
).resolves.toEqual([{ id: 11 }, { id: 12 }]);
|
|
expect(selectBuilder.orderBy).toHaveBeenCalled();
|
|
expect(selectBuilder.limit).toHaveBeenCalledWith(2);
|
|
});
|
|
|
|
it('findAll returns paged rows and scalar total using status/search filters', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.where.mockReturnValueOnce(selectBuilder).mockResolvedValueOnce([{ total: 7 }]);
|
|
selectBuilder.offset.mockResolvedValueOnce([{ id: 1 }, { id: 2 }]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(
|
|
repo.findAll({
|
|
status: 'pending',
|
|
page: 2,
|
|
limit: 2,
|
|
sort: 'fileName',
|
|
order: 'asc',
|
|
search: 'dune',
|
|
userId: 1,
|
|
isSuperuser: true,
|
|
}),
|
|
).resolves.toEqual({
|
|
items: [{ id: 1 }, { id: 2 }],
|
|
total: 7,
|
|
});
|
|
});
|
|
|
|
it('findById and findByAbsolutePath return the first selected row', async () => {
|
|
const { db, selectBuilder } = makeDb();
|
|
selectBuilder.limit.mockResolvedValueOnce([{ id: 9 }]).mockResolvedValueOnce([{ id: 10 }]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.findById(9)).resolves.toEqual({ id: 9 });
|
|
await expect(repo.findByAbsolutePath('/bucket/10.epub')).resolves.toEqual({ id: 10 });
|
|
});
|
|
|
|
it('create and update return first returning row or undefined when update misses', async () => {
|
|
const { db, insertBuilder, updateBuilder } = makeDb();
|
|
insertBuilder.returning.mockResolvedValueOnce([{ id: 1, fileName: 'book.epub' }]);
|
|
updateBuilder.returning.mockResolvedValueOnce([{ id: 1, status: 'ready' }]).mockResolvedValueOnce([]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.create({ fileName: 'book.epub' } as never)).resolves.toEqual({ id: 1, fileName: 'book.epub' });
|
|
await expect(repo.update(1, { status: 'ready' } as never)).resolves.toEqual({ id: 1, status: 'ready' });
|
|
await expect(repo.update(999, { status: 'error' } as never)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('deleteById and deleteByAbsolutePath issue delete queries', async () => {
|
|
const { db } = makeDb();
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await repo.deleteById(1);
|
|
await repo.deleteByAbsolutePath('/bucket/book.epub');
|
|
|
|
expect(db.delete).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('findByIds and setTargetsByIds work for non-empty id sets', async () => {
|
|
const { db, selectBuilder, updateBuilder } = makeDb();
|
|
selectBuilder.where.mockResolvedValueOnce([{ id: 1 }, { id: 2 }]);
|
|
updateBuilder.returning.mockResolvedValueOnce([{ id: 1 }, { id: 2 }]);
|
|
const repo = new BookBucketRepository(db as never);
|
|
|
|
await expect(repo.findByIds([1, 2])).resolves.toEqual([{ id: 1 }, { id: 2 }]);
|
|
await expect(repo.setTargetsByIds([1, 2], 4, 5)).resolves.toBe(2);
|
|
});
|
|
});
|