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:
kikootwo
2026-05-15 06:27:42 -04:00
parent de72180bdd
commit 07fbff1133
2 changed files with 139 additions and 0 deletions
@@ -125,6 +125,72 @@ describe('processPlexRecentlyAddedCheck', () => {
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 () => {
const matcher = await import('@/lib/utils/audiobook-matcher');
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 () => {
configMock.getBackendMode.mockResolvedValue('audiobookshelf');
configMock.get.mockResolvedValue(null);