Files
frontend-app-authoring/src/library-authoring/create-library/data/apiHooks.test.tsx
David Ormsbee 225c82d037 fix: support "in progress" status for lib upload
When uploading a library archive file during the creation of a new
library, the code prior to this commit did not properly handle the "In
Progress" state, which is when the celery task doing the archive
processing is actively running. Note that this is distinct from the
"Pending" state, which is when the task is waiting in the queue to be
run (which in practice should almost never happen unless there is an
operational issue).

Since celery tasks run in-process during local development, the task
was always finished by the time that the browser made a call to check
on the status. The problem only happened on slower sandboxes, where
processing truly runs asynchronously and might take a few seconds.
Because this case wasn't handled, the frontend would never poll for
updates either, so the upload was basically lost as far as the user
was concerned.
2025-12-11 12:58:06 -05:00

248 lines
8.2 KiB
TypeScript

import React from 'react';
import { QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import { mockContentLibrary } from '@src/library-authoring/data/api.mocks';
import { initializeMocks } from '@src/testUtils';
import { libraryAuthoringQueryKeys } from '../../data/apiHooks';
import {
useCreateLibraryRestore,
useCreateLibraryV2,
useGetLibraryRestoreStatus,
} from './apiHooks';
import { LibraryRestoreStatus } from './restoreConstants';
mockContentLibrary.applyMock();
const { axiosMock, queryClient } = initializeMocks();
const wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
describe('create library apiHooks', () => {
beforeEach(() => {
queryClient.clear();
axiosMock.reset();
});
describe('useCreateLibraryV2', () => {
it('should create library and invalidate queries', async () => {
const libraryData = {
title: 'Test Library',
org: 'test-org',
slug: 'test-library',
learning_package: 1,
};
const expectedResult = {
id: 'lib:test-org:test-library',
title: 'Test Library',
org: 'test-org',
slug: 'test-library',
};
// Mock the API call
axiosMock.onPost('http://localhost:18010/api/libraries/v2/').reply(200, expectedResult);
// Spy on query invalidation
const invalidateQueriesSpy = jest.spyOn(queryClient, 'invalidateQueries');
const { result } = renderHook(() => useCreateLibraryV2(), { wrapper });
await result.current.mutateAsync(libraryData);
expect(axiosMock.history.post[0].url).toEqual('http://localhost:18010/api/libraries/v2/');
expect(JSON.parse(axiosMock.history.post[0].data)).toEqual({
description: '',
...libraryData,
});
// Check that queries are invalidated on success
expect(invalidateQueriesSpy).toHaveBeenCalledWith({
queryKey: libraryAuthoringQueryKeys.contentLibraryList(),
});
});
});
describe('useCreateLibraryRestore', () => {
it('should restore library from file', async () => {
const file = new File(['test content'], 'test.tar.gz', { type: 'application/gzip' });
const expectedResult = { taskId: 'test-task-id' };
axiosMock.onPost('http://localhost:18010/api/libraries/v2/restore/').reply(200, expectedResult);
const { result } = renderHook(() => useCreateLibraryRestore(), { wrapper });
const response = await result.current.mutateAsync(file);
expect(axiosMock.history.post[0].url).toEqual('http://localhost:18010/api/libraries/v2/restore/');
expect(axiosMock.history.post[0].data).toBeInstanceOf(FormData);
expect(response).toEqual(expectedResult);
});
it('should handle restore error', async () => {
const file = new File(['test content'], 'test.tar.gz', { type: 'application/gzip' });
axiosMock.onPost('http://localhost:18010/api/libraries/v2/restore/').reply(400, 'Bad Request');
const { result } = renderHook(() => useCreateLibraryRestore(), { wrapper });
await expect(result.current.mutateAsync(file)).rejects.toThrow();
});
});
describe('useGetLibraryRestoreStatus', () => {
it('should get restore status when taskId is provided', async () => {
const taskId = 'test-task-id';
const expectedResult = {
state: LibraryRestoreStatus.Succeeded,
result: {
learningPackageId: 123,
title: 'Test Library',
org: 'test-org',
slug: 'test-library',
key: 'lib:test-org:test-library',
archiveKey: 'archive-key',
containers: 1,
components: 5,
collections: 2,
sections: 1,
subsections: 1,
units: 1,
createdOnServer: '2024-01-01T00:00:00Z',
createdAt: '2024-01-01T00:00:00Z',
createdBy: {
username: 'testuser',
email: 'test@example.com',
},
},
error: null,
errorLog: null,
};
axiosMock.onGet(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`).reply(200, expectedResult);
const { result } = renderHook(() => useGetLibraryRestoreStatus(taskId), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual(expectedResult);
expect(axiosMock.history.get[0].url).toEqual(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`);
});
it('should not make request when taskId is empty', async () => {
const { result } = renderHook(() => useGetLibraryRestoreStatus(''), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toBeUndefined();
expect(axiosMock.history.get).toHaveLength(0);
});
it('should handle pending status with refetch interval', async () => {
const taskId = 'pending-task-id';
const pendingResult = {
state: LibraryRestoreStatus.Pending,
result: null,
error: null,
error_log: null,
};
const expectedResult = {
state: LibraryRestoreStatus.Pending,
result: null,
error: null,
errorLog: null,
};
axiosMock.onGet(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`).reply(200, pendingResult);
const { result } = renderHook(() => useGetLibraryRestoreStatus(taskId), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual(expectedResult);
expect(axiosMock.history.get[0].url).toEqual(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`);
});
it('should handle in-progress status with refetch interval', async () => {
const taskId = 'in-progress-task-id';
const inProgressResult = {
state: LibraryRestoreStatus.InProgress,
result: null,
error: null,
error_log: null,
};
const expectedResult = {
state: LibraryRestoreStatus.InProgress,
result: null,
error: null,
errorLog: null,
};
axiosMock.onGet(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`).reply(200, inProgressResult);
const { result } = renderHook(() => useGetLibraryRestoreStatus(taskId), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual(expectedResult);
expect(axiosMock.history.get[0].url).toEqual(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`);
});
it('should handle failed status', async () => {
const taskId = 'failed-task-id';
const failedResult = {
state: LibraryRestoreStatus.Failed,
result: null,
error: 'Restore failed',
error_log: 'Error details here',
};
const expectedResult = {
state: LibraryRestoreStatus.Failed,
result: null,
error: 'Restore failed',
errorLog: 'Error details here',
};
axiosMock.onGet(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`).reply(200, failedResult);
const { result } = renderHook(() => useGetLibraryRestoreStatus(taskId), { wrapper });
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual(expectedResult);
expect(axiosMock.history.get[0].url).toEqual(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`);
});
it('should handle API error', async () => {
const taskId = 'error-task-id';
axiosMock.onGet(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`).reply(404, 'Not Found');
const { result } = renderHook(() => useGetLibraryRestoreStatus(taskId), { wrapper });
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
expect(result.current.data).toBeUndefined();
expect(axiosMock.history.get[0].url).toEqual(`http://localhost:18010/api/libraries/v2/restore/?task_id=${taskId}`);
});
});
});