Implement file hash-based library matching and remove fuzzy ASIN matching

Adds file hash-based matching for Audiobookshelf library items to ensure 100% accurate ASIN assignment for RMAB-organized content. Removes fuzzy matching from library availability checks, making all matching ASIN-only to eliminate false positives and race conditions. Updates database schema, processors, and matcher utilities; adds new tests and documentation for the new matching strategy. Removes obsolete scripts, Dockerfile, and related tests; updates docker-compose for test environments.
This commit is contained in:
kikootwo
2026-01-28 10:32:14 -05:00
parent 497849f427
commit a97979358f
111 changed files with 6571 additions and 1426 deletions
@@ -0,0 +1,156 @@
/**
* Component: Admin Settings Global Hook Tests
* Documentation: documentation/settings-pages.md
*/
// @vitest-environment jsdom
import React from 'react';
import { act, render, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const fetchWithAuthMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/utils/api', () => ({
fetchWithAuth: fetchWithAuthMock,
}));
const renderHook = <T,>(hook: () => T) => {
const result = { current: undefined as T };
function Probe() {
result.current = hook();
return null;
}
render(<Probe />);
return result;
};
const baseSettings = {
backendMode: 'plex',
hasLocalUsers: true,
audibleRegion: 'us',
plex: { url: '', token: '', libraryId: '', triggerScanAfterImport: false },
audiobookshelf: { serverUrl: '', apiToken: '', libraryId: '', triggerScanAfterImport: false },
oidc: {
enabled: false,
providerName: '',
issuerUrl: '',
clientId: '',
clientSecret: '',
accessControlMethod: 'open',
accessGroupClaim: 'groups',
accessGroupValue: '',
allowedEmails: '["first@example.com","second@example.com"]',
allowedUsernames: '["alpha","beta"]',
adminClaimEnabled: false,
adminClaimName: 'groups',
adminClaimValue: '',
},
registration: { enabled: false, requireAdminApproval: false },
prowlarr: { url: '', apiKey: '' },
downloadClient: {
type: 'qbittorrent',
url: '',
username: '',
password: '',
disableSSLVerify: false,
remotePathMappingEnabled: false,
remotePath: '',
localPath: '',
},
paths: {
downloadDir: '',
mediaDir: '',
audiobookPathTemplate: '',
metadataTaggingEnabled: true,
chapterMergingEnabled: false,
},
ebook: { enabled: false, preferredFormat: '', baseUrl: '', flaresolverrUrl: '' },
};
describe('useSettings', () => {
beforeEach(() => {
fetchWithAuthMock.mockReset();
});
it('loads settings and converts OIDC lists to comma-separated strings', async () => {
fetchWithAuthMock.mockResolvedValueOnce({
ok: true,
json: async () => baseSettings,
});
const { useSettings } = await import('@/app/admin/settings/hooks/useSettings');
const result = renderHook(() => useSettings());
await waitFor(() => expect(result.current.loading).toBe(false));
expect(result.current.settings?.oidc.allowedEmails).toBe('first@example.com, second@example.com');
expect(result.current.settings?.oidc.allowedUsernames).toBe('alpha, beta');
expect(fetchWithAuthMock).toHaveBeenCalledWith('/api/admin/settings');
});
it('tracks changes, resets, and marks settings as saved', async () => {
fetchWithAuthMock.mockResolvedValueOnce({
ok: true,
json: async () => baseSettings,
});
const { useSettings } = await import('@/app/admin/settings/hooks/useSettings');
const result = renderHook(() => useSettings());
await waitFor(() => expect(result.current.settings).not.toBeNull());
act(() => {
result.current.updateSettings({ audibleRegion: 'uk' });
});
expect(result.current.hasUnsavedChanges()).toBe(true);
act(() => {
result.current.resetSettings();
});
expect(result.current.settings?.audibleRegion).toBe('us');
expect(result.current.hasUnsavedChanges()).toBe(false);
act(() => {
result.current.updateSettings((prev) => ({ ...prev, audibleRegion: 'ca' }));
});
expect(result.current.hasUnsavedChanges()).toBe(true);
act(() => {
result.current.markAsSaved();
});
expect(result.current.hasUnsavedChanges()).toBe(false);
});
it('updates validation, test results, and message state', async () => {
fetchWithAuthMock.mockResolvedValueOnce({
ok: true,
json: async () => baseSettings,
});
const { useSettings } = await import('@/app/admin/settings/hooks/useSettings');
const result = renderHook(() => useSettings());
await waitFor(() => expect(result.current.settings).not.toBeNull());
act(() => {
result.current.updateValidation('plex', true);
result.current.updateTestResults('plex', { success: true, message: 'ok' });
result.current.showMessage({ type: 'success', text: 'Saved' });
});
expect(result.current.validated.plex).toBe(true);
expect(result.current.testResults.plex).toEqual({ success: true, message: 'ok' });
expect(result.current.message?.text).toBe('Saved');
act(() => {
result.current.clearMessage();
});
expect(result.current.message).toBeNull();
});
});