mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add tests for BigInt duration overflow (Plex)
Add regression tests to verify durations exceeding INT4 max are persisted as BigInt for Plex flows. Tests added in plex-recently-added.processor.test.ts and scan-plex.processor.test.ts cover both create and update paths (regression #193), mock the observed overflow (~4,082,750s → 4,082,750,000ms) and assert prisma.create/prisma.update are called with BigInt duration values.
This commit is contained in:
@@ -125,6 +125,72 @@ describe('processPlexRecentlyAddedCheck', () => {
|
|||||||
expect(prismaMock.plexLibrary.update).toHaveBeenCalled();
|
expect(prismaMock.plexLibrary.update).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('persists durations exceeding INT4 max as BigInt on both create and update paths (regression for #193)', async () => {
|
||||||
|
configMock.getBackendMode.mockResolvedValue('plex');
|
||||||
|
configMock.getMany.mockResolvedValue({
|
||||||
|
plex_url: 'http://plex',
|
||||||
|
plex_token: 'token',
|
||||||
|
plex_audiobook_library_id: 'lib-1',
|
||||||
|
});
|
||||||
|
configMock.get.mockResolvedValue('lib-1');
|
||||||
|
|
||||||
|
libraryServiceMock.getCoverCachingParams.mockResolvedValue({
|
||||||
|
backendBaseUrl: 'http://plex',
|
||||||
|
authToken: 'token',
|
||||||
|
backendMode: 'plex',
|
||||||
|
});
|
||||||
|
|
||||||
|
thumbnailCacheServiceMock.cacheLibraryThumbnail.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Production-observed overflow value: ~4_082_750 seconds → 4_082_750_000 ms (> INT4 max 2_147_483_647)
|
||||||
|
const overflowSeconds = 4_082_750;
|
||||||
|
const overflowMs = BigInt(overflowSeconds * 1000);
|
||||||
|
|
||||||
|
libraryServiceMock.getRecentlyAdded.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'rating-new',
|
||||||
|
externalId: 'guid-new',
|
||||||
|
title: 'Long Audiobook (new)',
|
||||||
|
author: 'Author',
|
||||||
|
duration: overflowSeconds,
|
||||||
|
addedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating-existing',
|
||||||
|
externalId: 'guid-existing',
|
||||||
|
title: 'Long Audiobook (existing)',
|
||||||
|
author: 'Author',
|
||||||
|
duration: overflowSeconds,
|
||||||
|
addedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
prismaMock.plexLibrary.findUnique.mockImplementation(async (query: any) => {
|
||||||
|
if (query.where.plexGuid === 'guid-existing') {
|
||||||
|
return { id: 'existing-id', plexGuid: 'guid-existing', author: 'Author', duration: null };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
prismaMock.plexLibrary.create.mockResolvedValue({});
|
||||||
|
prismaMock.plexLibrary.update.mockResolvedValue({});
|
||||||
|
prismaMock.request.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const { processPlexRecentlyAddedCheck } = await import('@/lib/processors/plex-recently-added.processor');
|
||||||
|
await processPlexRecentlyAddedCheck({ jobId: 'job-overflow' });
|
||||||
|
|
||||||
|
expect(prismaMock.plexLibrary.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({ duration: overflowMs }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(prismaMock.plexLibrary.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: { plexGuid: 'guid-existing' },
|
||||||
|
data: expect.objectContaining({ duration: overflowMs }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('matches requests without re-triggering ABS metadata match for audiobookshelf', async () => {
|
it('matches requests without re-triggering ABS metadata match for audiobookshelf', async () => {
|
||||||
const matcher = await import('@/lib/utils/audiobook-matcher');
|
const matcher = await import('@/lib/utils/audiobook-matcher');
|
||||||
const absApi = await import('@/lib/services/audiobookshelf/api');
|
const absApi = await import('@/lib/services/audiobookshelf/api');
|
||||||
|
|||||||
@@ -140,6 +140,79 @@ describe('processScanPlex', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('persists durations exceeding INT4 max as BigInt on both create and update paths (regression for #193)', async () => {
|
||||||
|
configMock.getBackendMode.mockResolvedValue('plex');
|
||||||
|
configMock.getPlexConfig.mockResolvedValue({
|
||||||
|
serverUrl: 'http://plex',
|
||||||
|
authToken: 'token',
|
||||||
|
libraryId: 'lib-1',
|
||||||
|
machineIdentifier: 'machine',
|
||||||
|
});
|
||||||
|
|
||||||
|
libraryServiceMock.getCoverCachingParams.mockResolvedValue({
|
||||||
|
backendBaseUrl: 'http://plex',
|
||||||
|
authToken: 'token',
|
||||||
|
backendMode: 'plex',
|
||||||
|
});
|
||||||
|
|
||||||
|
thumbnailCacheServiceMock.cacheLibraryThumbnail.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Production-observed overflow value: ~4_082_750 seconds → 4_082_750_000 ms (> INT4 max 2_147_483_647)
|
||||||
|
const overflowSeconds = 4_082_750;
|
||||||
|
const overflowMs = BigInt(overflowSeconds * 1000);
|
||||||
|
|
||||||
|
libraryServiceMock.getLibraryItems.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'rating-new',
|
||||||
|
externalId: 'guid-new',
|
||||||
|
title: 'Long Audiobook (new)',
|
||||||
|
author: 'Author',
|
||||||
|
duration: overflowSeconds,
|
||||||
|
addedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating-existing',
|
||||||
|
externalId: 'guid-existing',
|
||||||
|
title: 'Long Audiobook (existing)',
|
||||||
|
author: 'Author',
|
||||||
|
duration: overflowSeconds,
|
||||||
|
addedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
prismaMock.plexLibrary.findFirst.mockImplementation(async (query: any) => {
|
||||||
|
if (query.where.plexGuid === 'guid-existing') {
|
||||||
|
return { id: 'existing-id', plexGuid: 'guid-existing', author: 'Author', duration: null };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
prismaMock.plexLibrary.create.mockResolvedValue({ id: 'new-id', plexGuid: 'guid-new' });
|
||||||
|
prismaMock.plexLibrary.update.mockResolvedValue({});
|
||||||
|
prismaMock.plexLibrary.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.audiobook.findMany.mockResolvedValue([]);
|
||||||
|
prismaMock.request.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const matcher = await import('@/lib/utils/audiobook-matcher');
|
||||||
|
(matcher.findPlexMatch as ReturnType<typeof vi.fn>).mockResolvedValue(null);
|
||||||
|
|
||||||
|
const { processScanPlex } = await import('@/lib/processors/scan-plex.processor');
|
||||||
|
await processScanPlex({ jobId: 'job-overflow' });
|
||||||
|
|
||||||
|
expect(prismaMock.plexLibrary.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: expect.objectContaining({ duration: overflowMs }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(prismaMock.plexLibrary.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: { id: 'existing-id' },
|
||||||
|
data: expect.objectContaining({ duration: overflowMs }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('throws when audiobookshelf library is not configured', async () => {
|
it('throws when audiobookshelf library is not configured', async () => {
|
||||||
configMock.getBackendMode.mockResolvedValue('audiobookshelf');
|
configMock.getBackendMode.mockResolvedValue('audiobookshelf');
|
||||||
configMock.get.mockResolvedValue(null);
|
configMock.get.mockResolvedValue(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user