mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-07-10 15:00:15 +00:00
Advance existing requests from Interactive Search
Pass requestId and requestedByUserId through request list and card components and into AudiobookDetailsModal. Add ADVANCEABLE_FROM_INTERACTIVE_SEARCH constant and logic so the Interactive Search modal routes to /api/requests/[id]/select-torrent for own/admin advanceable requests instead of creating a new request. Fix download-token usage to use the in-flight request id, extend Audiobook type with requestedByUserId, and add tests covering advance-vs-create routing and select-torrent behavior.
This commit is contained in:
@@ -409,6 +409,69 @@ describe('Request action routes', () => {
|
||||
expect(jobQueueMock.addNotificationJob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// awaiting_release happy-path coverage — Interactive Search → Download from
|
||||
// AudiobookDetailsModal now routes through select-torrent for own/admin
|
||||
// advanceable requests; the release-date gate is intentionally bypassed.
|
||||
it('selects a torrent and queues download from awaiting_release (admin)', async () => {
|
||||
authRequest.json.mockResolvedValue({ torrent: { title: 'Torrent', size: 100 } });
|
||||
prismaMock.request.findUnique.mockResolvedValueOnce({
|
||||
id: 'req-ar-admin',
|
||||
userId: 'user-1',
|
||||
status: 'awaiting_release',
|
||||
audiobook: { id: 'ab-ar-1', title: 'Title', author: 'Author' },
|
||||
} as any);
|
||||
prismaMock.user.findUnique.mockResolvedValueOnce({
|
||||
id: 'user-1',
|
||||
role: 'admin',
|
||||
autoApproveRequests: null,
|
||||
plexUsername: 'adminuser',
|
||||
} as any);
|
||||
prismaMock.request.update.mockResolvedValueOnce({
|
||||
id: 'req-ar-admin',
|
||||
status: 'downloading',
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
} as any);
|
||||
|
||||
const { POST } = await import('@/app/api/requests/[id]/select-torrent/route');
|
||||
const response = await POST({} as any, { params: Promise.resolve({ id: 'req-ar-admin' }) });
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(jobQueueMock.addDownloadJob).toHaveBeenCalled();
|
||||
expect(jobQueueMock.addNotificationJob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stores selected torrent on awaiting_release when approval is required', async () => {
|
||||
authRequest.json.mockResolvedValue({ torrent: { title: 'Torrent', size: 100 } });
|
||||
configState.values.set('auto_approve_requests', 'false');
|
||||
prismaMock.request.findUnique.mockResolvedValueOnce({
|
||||
id: 'req-ar-user',
|
||||
userId: 'user-1',
|
||||
status: 'awaiting_release',
|
||||
audiobook: { id: 'ab-ar-2', title: 'Title', author: 'Author' },
|
||||
} as any);
|
||||
prismaMock.user.findUnique.mockResolvedValueOnce({
|
||||
id: 'user-1',
|
||||
role: 'user',
|
||||
autoApproveRequests: null,
|
||||
plexUsername: 'plexuser',
|
||||
} as any);
|
||||
prismaMock.request.update.mockResolvedValueOnce({
|
||||
id: 'req-ar-user',
|
||||
status: 'awaiting_approval',
|
||||
audiobook: { title: 'Title', author: 'Author' },
|
||||
} as any);
|
||||
|
||||
const { POST } = await import('@/app/api/requests/[id]/select-torrent/route');
|
||||
const response = await POST({} as any, { params: Promise.resolve({ id: 'req-ar-user' }) });
|
||||
const payload = await response.json();
|
||||
|
||||
expect(payload.success).toBe(true);
|
||||
expect(payload.message).toMatch(/approval/i);
|
||||
expect(jobQueueMock.addDownloadJob).not.toHaveBeenCalled();
|
||||
expect(jobQueueMock.addNotificationJob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns error when ebook sidecar is disabled', async () => {
|
||||
configState.values.set('ebook_sidecar_enabled', 'false');
|
||||
|
||||
|
||||
@@ -38,8 +38,12 @@ vi.mock('@/lib/hooks/useRequests', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('@/components/requests/InteractiveTorrentSearchModal', () => ({
|
||||
InteractiveTorrentSearchModal: ({ isOpen }: { isOpen: boolean }) => (
|
||||
<div data-testid="interactive-modal" data-open={String(isOpen)} />
|
||||
InteractiveTorrentSearchModal: ({ isOpen, requestId }: { isOpen: boolean; requestId?: string }) => (
|
||||
<div
|
||||
data-testid="interactive-modal"
|
||||
data-open={String(isOpen)}
|
||||
data-request-id={requestId ?? ''}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -327,4 +331,88 @@ describe('AudiobookDetailsModal', () => {
|
||||
expect(screen.getByTitle('Interactive Search')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Manual Import')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('Interactive Search routing (advance vs. create)', () => {
|
||||
const openInteractiveAndReadForwardedRequestId = async (props: {
|
||||
user: { id: string; username: string; role?: string };
|
||||
requestStatus?: string | null;
|
||||
requestId?: string | null;
|
||||
requestedByUserId?: string | null;
|
||||
}) => {
|
||||
useAuthMock.mockReturnValue({ user: props.user });
|
||||
const { AudiobookDetailsModal } = await import('@/components/audiobooks/AudiobookDetailsModal');
|
||||
|
||||
render(
|
||||
<AudiobookDetailsModal
|
||||
asin="ASIN123"
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
requestStatus={props.requestStatus ?? null}
|
||||
requestId={props.requestId ?? null}
|
||||
requestedByUserId={props.requestedByUserId ?? null}
|
||||
/>
|
||||
);
|
||||
|
||||
await act(async () => {});
|
||||
fireEvent.click(screen.getByTitle('Interactive Search'));
|
||||
const modal = screen.getByTestId('interactive-modal');
|
||||
return modal.getAttribute('data-request-id') ?? '';
|
||||
};
|
||||
|
||||
it.each(['pending', 'failed', 'awaiting_search', 'awaiting_release'])(
|
||||
'forwards requestId when own user has an advanceable %s request',
|
||||
async (status) => {
|
||||
const forwarded = await openInteractiveAndReadForwardedRequestId({
|
||||
user: { id: 'user-1', username: 'u' },
|
||||
requestStatus: status,
|
||||
requestId: 'req-advance',
|
||||
requestedByUserId: 'user-1',
|
||||
});
|
||||
expect(forwarded).toBe('req-advance');
|
||||
}
|
||||
);
|
||||
|
||||
it.each(['awaiting_approval', 'searching', 'downloading', 'processing', 'denied'])(
|
||||
'does NOT forward requestId when own status is %s (blocked / non-advanceable)',
|
||||
async (status) => {
|
||||
const forwarded = await openInteractiveAndReadForwardedRequestId({
|
||||
user: { id: 'user-1', username: 'u' },
|
||||
requestStatus: status,
|
||||
requestId: 'req-x',
|
||||
requestedByUserId: 'user-1',
|
||||
});
|
||||
expect(forwarded).toBe('');
|
||||
}
|
||||
);
|
||||
|
||||
it('does NOT forward requestId for a non-admin viewing another user\'s awaiting_search request', async () => {
|
||||
const forwarded = await openInteractiveAndReadForwardedRequestId({
|
||||
user: { id: 'user-2', username: 'other' },
|
||||
requestStatus: 'awaiting_search',
|
||||
requestId: 'req-from-user-1',
|
||||
requestedByUserId: 'user-1',
|
||||
});
|
||||
expect(forwarded).toBe('');
|
||||
});
|
||||
|
||||
it('forwards requestId for an admin viewing another user\'s awaiting_search request', async () => {
|
||||
const forwarded = await openInteractiveAndReadForwardedRequestId({
|
||||
user: { id: 'admin-1', username: 'admin', role: 'admin' },
|
||||
requestStatus: 'awaiting_search',
|
||||
requestId: 'req-from-user-1',
|
||||
requestedByUserId: 'user-1',
|
||||
});
|
||||
expect(forwarded).toBe('req-from-user-1');
|
||||
});
|
||||
|
||||
it('does NOT forward requestId when caller omits requestId entirely', async () => {
|
||||
const forwarded = await openInteractiveAndReadForwardedRequestId({
|
||||
user: { id: 'user-1', username: 'u' },
|
||||
requestStatus: 'awaiting_search',
|
||||
requestId: null,
|
||||
requestedByUserId: 'user-1',
|
||||
});
|
||||
expect(forwarded).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,6 +118,9 @@ describe('InteractiveTorrentSearchModal', () => {
|
||||
await waitFor(() => {
|
||||
expect(selectTorrentMock).toHaveBeenCalledWith('req-123', baseResult);
|
||||
});
|
||||
// When requestId is set, the modal must NOT fall into the create-new-request
|
||||
// branch — that's the routing fix for own-request advanceable states.
|
||||
expect(requestWithTorrentMock).not.toHaveBeenCalled();
|
||||
expect(onSuccess).toHaveBeenCalled();
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user