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:
kikootwo
2026-05-26 09:17:57 -04:00
parent 96aa01a3ec
commit 97d5aeb651
9 changed files with 209 additions and 8 deletions
@@ -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();
});