Files
ReadMeABook/tests/app/admin/settings/tabs/EbookTab/useEbookSettings.test.tsx
T
kikootwo 95e63dfc36 Add ROOTLESS_CONTAINER and request UI updates
Introduce ROOTLESS_CONTAINER env to opt out of gosu (replace /proc uid_map detection) and update entrypoint messaging; adjust app-start.sh and redis-start.sh to skip gosu when ROOTLESS_CONTAINER=true and warn on UID/GID mismatch only when applicable. Backend: include audiobook audibleAsin in admin requests response (mapped to asin) and pass baseUrl through test-flaresolverr endpoint to the FlareSolverr tester. Frontend: RecentRequestsTable and RequestActionsDropdown now surface asin, accept/passthrough annasArchiveBaseUrl, and add a "View Details" flow using AudiobookDetailsModal; admin page passes ebook baseUrl from settings. InteractiveTorrentSearchModal refactor: improved UX/UI, keyboard handling, portal/modal mounting, skeleton/loading states, formatting helpers, and richer result display. Tests updated to match changes.
2026-02-06 17:13:39 -05:00

154 lines
4.9 KiB
TypeScript

/**
* Component: Ebook Settings Hook Tests
* Documentation: documentation/settings-pages.md
*/
// @vitest-environment jsdom
import React from 'react';
import { act, render } 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 baseEbook = {
enabled: true,
preferredFormat: 'epub',
baseUrl: 'https://annas-archive.li',
flaresolverrUrl: 'http://flare',
};
describe('useEbookSettings', () => {
const onChange = vi.fn();
const onSuccess = vi.fn();
const onError = vi.fn();
const markAsSaved = vi.fn();
beforeEach(() => {
fetchWithAuthMock.mockReset();
onChange.mockReset();
onSuccess.mockReset();
onError.mockReset();
markAsSaved.mockReset();
vi.useRealTimers();
});
it('updates ebook settings and clears flaresolverr test results when URL changes', async () => {
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: baseEbook, onChange, onSuccess, onError, markAsSaved })
);
act(() => {
result.current.updateEbook('flaresolverrUrl', 'http://new');
});
expect(onChange).toHaveBeenCalledWith({ ...baseEbook, flaresolverrUrl: 'http://new' });
expect(result.current.flaresolverrTestResult).toBeNull();
});
it('returns an error when testing FlareSolverr without a URL', async () => {
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: { ...baseEbook, flaresolverrUrl: '' }, onChange, onSuccess, onError, markAsSaved })
);
await act(async () => {
await result.current.testFlaresolverrConnection();
});
expect(result.current.flaresolverrTestResult?.success).toBe(false);
expect(result.current.flaresolverrTestResult?.message).toContain('Please enter a FlareSolverr URL');
});
it('tests FlareSolverr connection successfully and sends baseUrl', async () => {
fetchWithAuthMock.mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true, message: 'OK' }),
});
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: baseEbook, onChange, onSuccess, onError, markAsSaved })
);
await act(async () => {
await result.current.testFlaresolverrConnection();
});
expect(result.current.flaresolverrTestResult?.success).toBe(true);
// Verify baseUrl is included in the request body
const callBody = JSON.parse(fetchWithAuthMock.mock.calls[0][1].body);
expect(callBody.baseUrl).toBe('https://annas-archive.li');
expect(callBody.url).toBe('http://flare');
});
it('handles FlareSolverr test failures', async () => {
fetchWithAuthMock.mockRejectedValueOnce(new Error('flare down'));
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: baseEbook, onChange, onSuccess, onError, markAsSaved })
);
await act(async () => {
await result.current.testFlaresolverrConnection();
});
expect(result.current.flaresolverrTestResult?.message).toBe('flare down');
});
it('saves ebook settings and clears success banner after delay', async () => {
vi.useFakeTimers();
fetchWithAuthMock.mockResolvedValueOnce({ ok: true, json: async () => ({}) });
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: baseEbook, onChange, onSuccess, onError, markAsSaved })
);
await act(async () => {
await result.current.saveSettings();
});
expect(onSuccess).toHaveBeenCalledWith('E-book sidecar settings saved successfully!');
expect(markAsSaved).toHaveBeenCalled();
act(() => {
vi.advanceTimersByTime(3000);
});
expect(onSuccess).toHaveBeenCalledWith('');
vi.useRealTimers();
});
it('surfaces save errors', async () => {
fetchWithAuthMock.mockResolvedValueOnce({ ok: false, json: async () => ({}) });
const { useEbookSettings } = await import('@/app/admin/settings/tabs/EbookTab/useEbookSettings');
const result = renderHook(() =>
useEbookSettings({ ebook: baseEbook, onChange, onSuccess, onError, markAsSaved })
);
await act(async () => {
await result.current.saveSettings();
});
expect(onError).toHaveBeenCalledWith('Failed to save e-book settings');
});
});