diff --git a/static/js/publisher/hooks/__tests__/useSerialLogs.test.tsx b/static/js/publisher/hooks/__tests__/useSerialLogs.test.tsx index 31c58c1dd7..4a0f58d5d7 100644 --- a/static/js/publisher/hooks/__tests__/useSerialLogs.test.tsx +++ b/static/js/publisher/hooks/__tests__/useSerialLogs.test.tsx @@ -90,6 +90,168 @@ describe("useSerialLogs", () => { }); }); + test("returns serial logs data with pageSize param", async () => { + server.use( + http.get( + "/api/store/test-brand-id/models/test-model/serial-log", + ({ request }) => { + const url = new URL(request.url); + + expect(url.searchParams.get("page-size")).toBe("10"); + return HttpResponse.json({ + data: serialLogsResponse, + success: true, + }); + }, + ), + ); + + const { result } = renderHook( + () => + useSerialLogs("test-brand-id", "test-model", { + pageSize: 10, + }), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual({ + data: serialLogsResponse, + success: true, + }); + }); + + test("returns serial logs data with startTime and endTime params", async () => { + server.use( + http.get( + "/api/store/test-brand-id/models/test-model/serial-log", + ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("start-time")).toBe( + "2026-03-24T04:00:23.875000", + ); + expect(url.searchParams.get("end-time")).toBe( + "2026-03-28T04:00:23.875000", + ); + return HttpResponse.json({ + data: serialLogsResponse, + success: true, + }); + }, + ), + ); + + const { result } = renderHook( + () => + useSerialLogs("test-brand-id", "test-model", { + interval: { + startTime: "2026-03-24T04:00:23.875000", + endTime: "2026-03-28T04:00:23.875000", + }, + }), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual({ + data: serialLogsResponse, + success: true, + }); + }); + + test("returns serial logs data with nextPage param", async () => { + server.use( + http.get( + "/api/store/test-brand-id/models/test-model/serial-log", + ({ request }) => { + const url = new URL(request.url); + + expect(url.searchParams.get("next-page")).toBe("nextpagecursor"); + return HttpResponse.json({ + data: serialLogsResponse, + success: true, + }); + }, + ), + ); + + const { result } = renderHook( + () => + useSerialLogs("test-brand-id", "test-model", { + nextPage: "nextpagecursor", + }), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual({ + data: serialLogsResponse, + success: true, + }); + }); + + test("returns serial logs data with startTime, endTime, pageSize and nextPage params", async () => { + server.use( + http.get( + "/api/store/test-brand-id/models/test-model/serial-log", + ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("start-time")).toBe( + "2026-03-24T04:00:23.875000", + ); + expect(url.searchParams.get("end-time")).toBe( + "2026-03-28T04:00:23.875000", + ); + expect(url.searchParams.get("page-size")).toBe("10"); + expect(url.searchParams.get("next-page")).toBe("nextpagecursor"); + return HttpResponse.json({ + data: serialLogsResponse, + success: true, + }); + }, + ), + ); + + const { result } = renderHook( + () => + useSerialLogs("test-brand-id", "test-model", { + interval: { + startTime: "2026-03-24T04:00:23.875000", + endTime: "2026-03-28T04:00:23.875000", + }, + pageSize: 10, + nextPage: "nextpagecursor", + }), + { + wrapper: createWrapper(), + }, + ); + + await waitFor(() => { + expect(result.current.isSuccess).toBe(true); + }); + + expect(result.current.data).toEqual({ + data: serialLogsResponse, + success: true, + }); + }); + test("returns error if request fails", async () => { const { result } = renderHook( () => useSerialLogs("test-brand-id-fail", "test-model"), diff --git a/static/js/publisher/hooks/useSerialLogs.ts b/static/js/publisher/hooks/useSerialLogs.ts index fad50688d3..de58835513 100644 --- a/static/js/publisher/hooks/useSerialLogs.ts +++ b/static/js/publisher/hooks/useSerialLogs.ts @@ -4,13 +4,41 @@ import type { SerialLogResponse, ApiResponse } from "../types/shared"; const useSerialLogs = ( brandId: string | undefined, modelId: string | undefined, + urlSearchParams?: { + pageSize?: number; + nextPage?: string; + interval?: { + startTime: string; + endTime: string; + }; + }, ): UseQueryResult, Error> => { + const url = new URL( + `/api/store/${brandId}/models/${modelId}/serial-log`, + window.location.origin, + ); + + if (urlSearchParams) { + const { interval, pageSize, nextPage } = urlSearchParams; + + if (interval) { + url.searchParams.set("start-time", interval.startTime); + url.searchParams.set("end-time", interval.endTime); + } + + if (pageSize) { + url.searchParams.set("page-size", pageSize.toString()); + } + + if (nextPage) { + url.searchParams.set("next-page", nextPage); + } + } + return useQuery, Error>({ - queryKey: ["serials", brandId, modelId], + queryKey: ["serials", brandId, modelId, urlSearchParams], queryFn: async () => { - const response = await fetch( - `/api/store/${brandId}/models/${modelId}/serial-log`, - ); + const response = await fetch(url.toString()); const responseData = await response.json(); return responseData; diff --git a/tests/endpoints/tests_models.py b/tests/endpoints/tests_models.py index 7cf41c6a72..f380b03af5 100644 --- a/tests/endpoints/tests_models.py +++ b/tests/endpoints/tests_models.py @@ -427,3 +427,142 @@ def test_get_serial_log_general_error(self, mock_get_serial_log): self.assertEqual(response.status_code, 500) self.assertFalse(data["success"]) self.assertEqual(data["message"], "Internal server error") + + @patch( + "canonicalwebteam.store_api.publishergw.PublisherGW" + + ".get_store_model_serial_logs" + ) + def test_get_serial_log_with_time_range(self, mock_get_serial_log): + mock_serial_log = [] + mock_get_serial_log.return_value = mock_serial_log + response = self.client.get( + "/api/store/1/models/test-model/serial-log" + "?start-time=2026-06-01T00:00:00Z" + "&end-time=2026-06-30T23:59:59Z" + ) + data = response.json + + self.assertEqual(response.status_code, 200) + self.assertTrue(data["success"]) + self.assertEqual(data["data"], []) + + mock_get_serial_log.assert_called_once() + # Arguments are passed positionally: + # (session, store_id, model_name, start_time, end_time, page_size) + self.assertEqual( + mock_get_serial_log.call_args.args[3], + "2026-06-01T00:00:00Z", + ) + self.assertEqual( + mock_get_serial_log.call_args.args[4], + "2026-06-30T23:59:59Z", + ) + + @patch( + "canonicalwebteam.store_api.publishergw.PublisherGW" + + ".get_store_model_serial_logs" + ) + def test_get_serial_log_with_page_size(self, mock_get_serial_log): + mock_serial_log = [ + { + "brand-id": "test-brand-id", + "created-at": "2026-03-23T04:00:23.875000", + "model-name": "test-model", + "serial": "test-serial", + } + ] + mock_get_serial_log.return_value = mock_serial_log + response = self.client.get( + "/api/store/1/models/test-model/serial-log?page-size=50" + ) + data = response.json + + self.assertEqual(response.status_code, 200) + self.assertTrue(data["success"]) + self.assertEqual(data["data"], mock_serial_log) + + mock_get_serial_log.assert_called_once() + # Arguments are passed positionally: + # session, store_id, model_name, start_time, + # end_time, page_size, cursor + self.assertEqual( + mock_get_serial_log.call_args.args[5], + "50", + ) + + @patch( + "canonicalwebteam.store_api.publishergw.PublisherGW" + + ".get_store_model_serial_logs" + ) + def test_get_serial_log_with_next_page(self, mock_get_serial_log): + mock_serial_log = [ + { + "brand-id": "test-brand-id", + "created-at": "2026-03-23T04:00:23.875000", + "model-name": "test-model", + "serial": "test-serial", + } + ] + mock_get_serial_log.return_value = mock_serial_log + response = self.client.get( + "/api/store/1/models/test-model/serial-log?next-page=nextpage" + ) + data = response.json + + self.assertEqual(response.status_code, 200) + self.assertTrue(data["success"]) + self.assertEqual(data["data"], mock_serial_log) + + mock_get_serial_log.assert_called_once() + # Arguments are passed positionally: + # session, store_id, model_name, start_time, + # end_time, page_size, cursor + self.assertEqual( + mock_get_serial_log.call_args.args[6], + "nextpage", + ) + + @patch( + "canonicalwebteam.store_api.publishergw.PublisherGW" + + ".get_store_model_serial_logs" + ) + def test_get_serial_log_with_all_parameters(self, mock_get_serial_log): + mock_serial_log = [ + { + "brand-id": "test-brand-id", + "created-at": "2026-03-23T04:00:23.875000", + "model-name": "test-model", + "serial": "test-serial", + } + ] + mock_get_serial_log.return_value = mock_serial_log + response = self.client.get( + "/api/store/1/models/test-model/serial-log" + "?start-time=2026-01-01T00:00:00Z" + "&end-time=2026-12-31T23:59:59Z" + "&page-size=25" + "&next-page=nextpage" + ) + data = response.json + + self.assertEqual(response.status_code, 200) + self.assertTrue(data["success"]) + self.assertEqual(data["data"], mock_serial_log) + + mock_get_serial_log.assert_called_once() + # Arguments are passed positionally: + # session, store_id, model_name, start_time, + # end_time, page_size, cursor + self.assertEqual( + mock_get_serial_log.call_args.args[3], + "2026-01-01T00:00:00Z", + ) + self.assertEqual( + mock_get_serial_log.call_args.args[4], + "2026-12-31T23:59:59Z", + ) + self.assertEqual( + mock_get_serial_log.call_args.args[5], + "25", + ) + self.assertEqual(mock_get_serial_log.call_args.args[6], "nextpage") diff --git a/webapp/endpoints/models.py b/webapp/endpoints/models.py index 1dc0b57ce3..6f2682a08b 100644 --- a/webapp/endpoints/models.py +++ b/webapp/endpoints/models.py @@ -353,6 +353,10 @@ def create_remodel_allowlist(store_id: str): @login_required @exchange_required def get_serial_log(store_id: str, model_name: str): + start_time = flask.request.args.get("start-time") + end_time = flask.request.args.get("end-time") + page_size = flask.request.args.get("page-size") + cursor = flask.request.args.get("next-page") res = {} try: @@ -360,6 +364,10 @@ def get_serial_log(store_id: str, model_name: str): flask.session, store_id, model_name, + start_time, + end_time, + page_size, + cursor, ) res["data"] = logs res["success"] = True