mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
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:
@@ -124,6 +124,7 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
|
||||
{
|
||||
id: 1,
|
||||
name: 'AudioBook Bay',
|
||||
protocol: 'torrent',
|
||||
priority: 10,
|
||||
seedingTimeMinutes: 4320,
|
||||
rssEnabled: true,
|
||||
@@ -132,8 +133,9 @@ describe('IndexersTab - Auto-load Indexers on Tab Activation', () => {
|
||||
{
|
||||
id: 2,
|
||||
name: 'MyAnonaMouse',
|
||||
protocol: 'usenet',
|
||||
priority: 15,
|
||||
seedingTimeMinutes: 10080,
|
||||
removeAfterProcessing: true,
|
||||
rssEnabled: false,
|
||||
categories: [3030],
|
||||
},
|
||||
|
||||
@@ -10,12 +10,14 @@ import { act, fireEvent, render, screen } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const swipeHandlers: {
|
||||
onSwipeStart?: () => void;
|
||||
onSwiping?: (eventData: { deltaX: number; deltaY: number }) => void;
|
||||
onSwiped?: (eventData: { deltaX: number; deltaY: number }) => void;
|
||||
} = {};
|
||||
|
||||
vi.mock('react-swipeable', () => ({
|
||||
useSwipeable: (handlers: any) => {
|
||||
swipeHandlers.onSwipeStart = handlers.onSwipeStart;
|
||||
swipeHandlers.onSwiping = handlers.onSwiping;
|
||||
swipeHandlers.onSwiped = handlers.onSwiped;
|
||||
return {};
|
||||
@@ -33,6 +35,7 @@ const recommendation = {
|
||||
|
||||
describe('RecommendationCard', () => {
|
||||
beforeEach(() => {
|
||||
swipeHandlers.onSwipeStart = undefined;
|
||||
swipeHandlers.onSwiping = undefined;
|
||||
swipeHandlers.onSwiped = undefined;
|
||||
});
|
||||
@@ -87,6 +90,7 @@ describe('RecommendationCard', () => {
|
||||
render(<RecommendationCard recommendation={recommendation} onSwipe={onSwipe} />);
|
||||
|
||||
act(() => {
|
||||
swipeHandlers.onSwipeStart?.();
|
||||
swipeHandlers.onSwiping?.({ deltaX: -80, deltaY: 0 });
|
||||
});
|
||||
|
||||
|
||||
@@ -223,4 +223,135 @@ describe('Header', () => {
|
||||
|
||||
expect(screen.queryByRole('link', { name: 'BookDate' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens change password modal and closes it', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
renderWithProviders(<Header />, {
|
||||
auth: {
|
||||
user: {
|
||||
id: 'user-4',
|
||||
plexId: 'plex-4',
|
||||
username: 'local-admin',
|
||||
role: 'admin',
|
||||
authProvider: 'local',
|
||||
},
|
||||
isLoading: false,
|
||||
},
|
||||
});
|
||||
|
||||
const userButton = screen.getByText('local-admin').closest('button');
|
||||
expect(userButton).not.toBeNull();
|
||||
await userEvent.click(userButton as HTMLButtonElement);
|
||||
|
||||
const changePasswordButton = await screen.findByText('Change Password');
|
||||
await userEvent.click(changePasswordButton);
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Change Password' })).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('heading', { name: 'Change Password' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('closes the user menu when profile is clicked', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
renderWithProviders(<Header />, {
|
||||
auth: {
|
||||
user: {
|
||||
id: 'user-5',
|
||||
plexId: 'plex-5',
|
||||
username: 'reader',
|
||||
role: 'user',
|
||||
authProvider: 'local',
|
||||
},
|
||||
isLoading: false,
|
||||
},
|
||||
});
|
||||
|
||||
const userButton = screen.getByText('reader').closest('button');
|
||||
expect(userButton).not.toBeNull();
|
||||
await userEvent.click(userButton as HTMLButtonElement);
|
||||
|
||||
const profileLink = await screen.findByRole('link', { name: 'Profile' });
|
||||
await userEvent.click(profileLink);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Logout')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('logs errors when Plex login fails', async () => {
|
||||
const fetchMock = vi.fn().mockRejectedValue(new Error('login failed'));
|
||||
const errorMock = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const openMock = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
renderWithProviders(<Header />, { auth: { user: null, isLoading: false } });
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /login with plex/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(errorMock).toHaveBeenCalledWith('Login failed:', expect.any(Error));
|
||||
});
|
||||
expect(openMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('closes the mobile menu when BookDate is selected', async () => {
|
||||
localStorage.setItem('accessToken', 'token');
|
||||
const fetchMock = vi.fn().mockImplementation((input: RequestInfo) => {
|
||||
if (input === '/api/version') {
|
||||
return Promise.resolve({
|
||||
json: vi.fn().mockResolvedValue({ version: 'v.test' }),
|
||||
});
|
||||
}
|
||||
if (input === '/api/bookdate/config') {
|
||||
return Promise.resolve({
|
||||
json: vi.fn().mockResolvedValue({
|
||||
config: { isVerified: true, isEnabled: true },
|
||||
}),
|
||||
});
|
||||
}
|
||||
return Promise.resolve({
|
||||
json: vi.fn().mockResolvedValue({}),
|
||||
});
|
||||
});
|
||||
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
renderWithProviders(<Header />, {
|
||||
auth: {
|
||||
user: {
|
||||
id: 'user-6',
|
||||
plexId: 'plex-6',
|
||||
username: 'reader',
|
||||
role: 'user',
|
||||
},
|
||||
isLoading: false,
|
||||
},
|
||||
});
|
||||
|
||||
const initialBookDateCount = (await screen.findAllByRole('link', { name: 'BookDate' })).length;
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Toggle menu' }));
|
||||
|
||||
const bookDateLinks = await screen.findAllByRole('link', { name: 'BookDate' });
|
||||
expect(bookDateLinks).toHaveLength(initialBookDateCount + 1);
|
||||
|
||||
await userEvent.click(bookDateLinks[bookDateLinks.length - 1]);
|
||||
|
||||
await waitFor(async () => {
|
||||
const remainingLinks = await screen.findAllByRole('link', { name: 'BookDate' });
|
||||
expect(remainingLinks).toHaveLength(initialBookDateCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/**
|
||||
* Component: Pagination Tests
|
||||
* Documentation: documentation/frontend/components.md
|
||||
*/
|
||||
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
|
||||
describe('Pagination', () => {
|
||||
it('renders nothing when there is only one page', () => {
|
||||
const { container } = render(<Pagination currentPage={1} totalPages={1} onPageChange={vi.fn()} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders ellipses for large page ranges', () => {
|
||||
render(<Pagination currentPage={5} totalPages={10} onPageChange={vi.fn()} />);
|
||||
const ellipses = screen.getAllByText('...');
|
||||
expect(ellipses.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('calls onPageChange for navigation controls', () => {
|
||||
const onPageChange = vi.fn();
|
||||
render(<Pagination currentPage={2} totalPages={5} onPageChange={onPageChange} />);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Previous page'));
|
||||
expect(onPageChange).toHaveBeenCalledWith(1);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Next page'));
|
||||
expect(onPageChange).toHaveBeenCalledWith(3);
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Page 4'));
|
||||
expect(onPageChange).toHaveBeenCalledWith(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user