From 4be6db27123babb9e9bc1b816529b8f3989a2dac Mon Sep 17 00:00:00 2001 From: bodoque-01 Date: Thu, 21 May 2026 04:47:14 -0300 Subject: [PATCH] searchApi: fix response interceptors and suggestion config validation Register response interceptors on the axios response pipeline in InvenioSearchApi and OSSearchApi. Call InvenioSuggestionApi.validateConfig from initSerializers and fix the responseField error message. Delete response.newQueryState after updating state from search results. --- .../api/contrib/invenio/InvenioSearchApi.js | 2 +- .../contrib/invenio/InvenioSearchApi.test.js | 32 ++++++++++ .../contrib/invenio/InvenioSuggestionApi.js | 8 ++- .../invenio/InvenioSuggestionApi.test.js | 64 +++++++++++++++++++ src/lib/api/contrib/opensearch/OSSearchApi.js | 2 +- .../contrib/opensearch/OSSearchApi.test.js | 32 ++++++++++ src/lib/state/actions/query.js | 2 +- 7 files changed, 136 insertions(+), 6 deletions(-) create mode 100644 src/lib/api/contrib/invenio/InvenioSuggestionApi.test.js diff --git a/src/lib/api/contrib/invenio/InvenioSearchApi.js b/src/lib/api/contrib/invenio/InvenioSearchApi.js index 49464673..a42fe95f 100644 --- a/src/lib/api/contrib/invenio/InvenioSearchApi.js +++ b/src/lib/api/contrib/invenio/InvenioSearchApi.js @@ -81,7 +81,7 @@ export class InvenioSearchApi { ); } if (this.responseInterceptor) { - this.http.interceptors.request.use( + this.http.interceptors.response.use( this.responseInterceptor.resolve, this.responseInterceptor.reject ); diff --git a/src/lib/api/contrib/invenio/InvenioSearchApi.test.js b/src/lib/api/contrib/invenio/InvenioSearchApi.test.js index 4e9be2a1..f7d56c34 100644 --- a/src/lib/api/contrib/invenio/InvenioSearchApi.test.js +++ b/src/lib/api/contrib/invenio/InvenioSearchApi.test.js @@ -76,4 +76,36 @@ describe("test InvenioSearchApi class", () => { const request = mockedAxios.history.get[0]; expect(request.url).toBe("/api/records"); }); + + it("should register response interceptors on the response pipeline", async () => { + const responseResolve = jest.fn((response) => response); + const searchApi = new InvenioSearchApi({ + axios: { + url: "/api/records", + }, + invenio: { + requestSerializer: MockedRequestSerializer, + responseSerializer: MockedResponseSerializer, + }, + interceptors: { + response: { + resolve: responseResolve, + reject: (error) => Promise.reject(error), + }, + }, + }); + const mockedAxios = new MockAdapter(searchApi.http); + + const mockedResponse = { hits: [{ result: "1" }] }; + mockedAxios.onAny().reply(200, mockedResponse); + await searchApi.search({ q: "test" }); + + expect(responseResolve).toHaveBeenCalledTimes(1); + expect(responseResolve).toHaveBeenCalledWith( + expect.objectContaining({ + status: 200, + data: mockedResponse, + }) + ); + }); }); diff --git a/src/lib/api/contrib/invenio/InvenioSuggestionApi.js b/src/lib/api/contrib/invenio/InvenioSuggestionApi.js index 23f2ef6f..fe9f149e 100644 --- a/src/lib/api/contrib/invenio/InvenioSuggestionApi.js +++ b/src/lib/api/contrib/invenio/InvenioSuggestionApi.js @@ -58,8 +58,9 @@ class InvenioSuggestionResponseSerializer { export class InvenioSuggestionApi extends InvenioSearchApi { validateConfig(config) { - super.validateConfig(config); - + // the parent `axios` config is already validated by the `InvenioSearchApi` + // constructor via `validateAxiosConfig`, here we only validate the + // suggestion-specific config if (!_hasIn(config, "invenio.suggestions.queryField")) { throw new Error( "InvenioSuggestionApi config: `invenio.suggestions.queryField` is required." @@ -67,12 +68,13 @@ export class InvenioSuggestionApi extends InvenioSearchApi { } if (!_hasIn(config, "invenio.suggestions.responseField")) { throw new Error( - "InvenioSuggestionApi config: `invenio.suggestions.queryField` is responseField." + "InvenioSuggestionApi config: `invenio.suggestions.responseField` is required." ); } } initSerializers(config) { + this.validateConfig(config); const requestSerializerCls = _get( config, "invenio.requestSerializer", diff --git a/src/lib/api/contrib/invenio/InvenioSuggestionApi.test.js b/src/lib/api/contrib/invenio/InvenioSuggestionApi.test.js new file mode 100644 index 00000000..93b12d01 --- /dev/null +++ b/src/lib/api/contrib/invenio/InvenioSuggestionApi.test.js @@ -0,0 +1,64 @@ +/* + * This file is part of React-SearchKit. + * Copyright (C) 2019 CERN. + * + * React-SearchKit is free software; you can redistribute it and/or modify it + * under the terms of the MIT License; see LICENSE file for more details. + */ + +import { InvenioSuggestionApi } from "."; + +describe("test InvenioSuggestionApi class", () => { + it("should throw when `invenio.suggestions.queryField` is missing", () => { + expect( + () => + new InvenioSuggestionApi({ + axios: { + url: "/api/records", + }, + invenio: { + suggestions: { + responseField: "metadata.title", + }, + }, + }) + ).toThrow( + "InvenioSuggestionApi config: `invenio.suggestions.queryField` is required." + ); + }); + + it("should throw when `invenio.suggestions.responseField` is missing", () => { + expect( + () => + new InvenioSuggestionApi({ + axios: { + url: "/api/records", + }, + invenio: { + suggestions: { + queryField: "title", + }, + }, + }) + ).toThrow( + "InvenioSuggestionApi config: `invenio.suggestions.responseField` is required." + ); + }); + + it("should not throw when both suggestion fields are provided", () => { + expect( + () => + new InvenioSuggestionApi({ + axios: { + url: "/api/records", + }, + invenio: { + suggestions: { + queryField: "title", + responseField: "metadata.title", + }, + }, + }) + ).not.toThrow(); + }); +}); diff --git a/src/lib/api/contrib/opensearch/OSSearchApi.js b/src/lib/api/contrib/opensearch/OSSearchApi.js index 1ae4ccc6..978133a4 100644 --- a/src/lib/api/contrib/opensearch/OSSearchApi.js +++ b/src/lib/api/contrib/opensearch/OSSearchApi.js @@ -64,7 +64,7 @@ export class OSSearchApi { ); } if (this.responseInterceptor) { - this.http.interceptors.request.use( + this.http.interceptors.response.use( this.responseInterceptor.resolve, this.responseInterceptor.reject ); diff --git a/src/lib/api/contrib/opensearch/OSSearchApi.test.js b/src/lib/api/contrib/opensearch/OSSearchApi.test.js index d3b7a7d3..4b6c2e29 100644 --- a/src/lib/api/contrib/opensearch/OSSearchApi.test.js +++ b/src/lib/api/contrib/opensearch/OSSearchApi.test.js @@ -76,4 +76,36 @@ describe("test OSSearchApi class", () => { const request = mockedAxios.history.post[0]; expect(request.url).toBe("/api/records"); }); + + it("should register response interceptors on the response pipeline", async () => { + const responseResolve = jest.fn((response) => response); + const searchApi = new OSSearchApi({ + axios: { + url: "/api/records", + }, + os: { + requestSerializer: MockedRequestSerializer, + responseSerializer: MockedResponseSerializer, + }, + interceptors: { + response: { + resolve: responseResolve, + reject: (error) => Promise.reject(error), + }, + }, + }); + const mockedAxios = new MockAdapter(searchApi.http); + + const mockedResponse = { hits: [{ result: "1" }] }; + mockedAxios.onAny().reply(200, mockedResponse); + await searchApi.search({ q: "test" }); + + expect(responseResolve).toHaveBeenCalledTimes(1); + expect(responseResolve).toHaveBeenCalledWith( + expect.objectContaining({ + status: 200, + data: mockedResponse, + }) + ); + }); }); diff --git a/src/lib/state/actions/query.js b/src/lib/state/actions/query.js index 1b331390..013e166f 100644 --- a/src/lib/state/actions/query.js +++ b/src/lib/state/actions/query.js @@ -199,7 +199,7 @@ const updateQueryStateAfterResponse = (response, dispatch, getState, appConfig) // Replace the URL args with the response new query state urlHandlerApi.replace(updatedQueryState); } - delete response.newStateQuery; + delete response.newQueryState; return response; };