import axios, { AxiosInstance } from "axios"; import AxiosMockAdapter from "axios-mock-adapter"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { TokenManager, createTokenManager } from "../token-manager/token-manager"; import { createAxiosInstance } from "./http-client"; import { BROKER_EVENTS } from "../broker/broker-events"; import { createBroker } from "../broker/factory"; const access_token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJidFZxNnRMWGpmcXdzbm5MR2FXMXdhdU9McDNiTmY4bWZ3Rm1SZ0lBS2VJIn0.eyJleHAiOjE3NDI5MDkxNjMsImlhdCI6MTc0MjkwODI2MywianRpIjoiODRlMWEwYjYtOGNmYS00NzQ3LTk0MDItMjVkM2FlMzM3N2QxIiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsInN1YiI6Ijk1OGUyZWQ5LTJkNmUtNDZjOC1hOTE5LTU5MDQ0ZTYwMzVjMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImV0Yy1jY2YtcHJlIiwic2Vzc2lvbl9zdGF0ZSI6ImVhNzUwMDg4LWI1NjctNDU4ZC1iOWY5LWQ0OTdjMzlkN2ZkMyIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWhlcyIsIlJPTEVfSEVTX0VUQyIsIlJPTEVfSEVTX0xBTkRJTkciXX0sInNjb3BlIjoiIiwic2lkIjoiZWE3NTAwODgtYjU2Ny00NThkLWI5ZjktZDQ5N2MzOWQ3ZmQzIiwiY2xpZW50SG9zdCI6IjEwLjUzLjI1NC4xNTAiLCJhY2Nlc3NfcnVsZXMiOnt9LCJhY2Nlc3NfaW5mbyI6eyJtb2R1bGVfY29kZSI6IkEwMDEiLCJyb2xlX3R5cGUiOiJOT1JNIiwidHJhY2VfdXNlcl9pZCI6IlVzZXJfSUQiLCJjZW50ZXJfdHlwZSI6IkUiLCJtcGlfcGF0aWVudF9pZCI6IjVlYzc0YzdiLTVhYTUtNGE4NC1iM2I0LWYwMmVkZGZkZjZkYyIsInVwX2NvZGUiOiIwNzczMyIsInRyYWNlX3VzZXJfZ2l2ZW5fbmFtZSI6IkdpdmVuIE5hbWUiLCJ0cmFjZV91c2VyX2ZhbWlseV9uYW1lIjoiRmFtaWx5IE5hbWUiLCJ1c2VyX3R5cGUiOiJBRE0iLCJjZW50ZXJfY29kZSI6IkUwODU4Njk2MyIsInByb2Zlc3Npb25hbF9jYXRlZ29yeSI6IjMwOTM0MzAwNiIsInNlcnZpY2VfY29kZSI6IjVTMDg5IiwiZXBfY29kZSI6IjAyMDgiLCJpZGVudGlmaWVyIjpbeyJ0eXBlIjoiRE5JIiwidmFsdWUiOiI3MzI4ODIxOUEifSx7InR5cGUiOiJOVU1DT0wiLCJ2YWx1ZSI6IjIifV0sImFsdF9pZGVudGlmaWVyIjpbeyJ0eXBlIjoiTVBJIiwidmFsdWUiOiIwNjIxY2Y3ZC03ZDYzLTQ5ZWMtODAwNi04YzA1NjkyZWQzNzcifV19LCJjbGllbnRBZGRyZXNzIjoiMTAuNTMuMjU0LjE1MCIsImNsaWVudF9pZCI6ImV0Yy1jY2YtcHJlIn0.CgYL3zXbUHHiE7ZtrcxvlDHB3dbY_eI8TsL-0mBOy-JedSZZaGT_JS8Odm_eIo1drWZSnEaNblFZUPE56sCEPH6RYSqtyK0eYwy_aKoprKoRuDjEy2-VfmOJi5gY9wpYnAYpVtuTbncVirQIV9SIZ9st2d7LlfcM_88Yn8NJNTDcUk7ETw7Zy_728X2wjL_UeHT7yuzo3Y-Xm_389Og7UMyYGGrs104CHQNrzIMXiklkRZr4dqjqWU61Csrb8OBI6jCUAbBqgS8vPuUzPUghQXstr-aNn3zicy-Zw5e6RqCmSQXg56aTKx9QIOrQuX-az-urPnqvtWdloQrQJLQGSg"; const refresh_token = "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwMjUzZGM1MC1hY2FmLTQ5ZDctYTYzNi0xN2NkMTlmOWEwOTAifQ.eyJleHAiOjE3NDI5MTE4NjMsImlhdCI6MTc0MjkwODI2MywianRpIjoiZjA4MzA1MDktMTA0My00ODI1LTk5YTItOTg1YmNjOGI4MmJmIiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsImF1ZCI6Imh0dHBzOi8vcHJlcHJvZHVjY2lvLnBkcy5oZXMuY2F0c2FsdXQuZ2VuY2F0LmNhdC9hdXRoL3JlYWxtcy9IRVMiLCJzdWIiOiI5NThlMmVkOS0yZDZlLTQ2YzgtYTkxOS01OTA0NGU2MDM1YzIiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoiZXRjLWNjZi1wcmUiLCJzZXNzaW9uX3N0YXRlIjoiZWE3NTAwODgtYjU2Ny00NThkLWI5ZjktZDQ5N2MzOWQ3ZmQzIiwic2NvcGUiOiIiLCJzaWQiOiJlYTc1MDA4OC1iNTY3LTQ1OGQtYjlmOS1kNDk3YzM5ZDdmZDMifQ.I8u83nfdxbjRrS7dC1k8_f9oBKmSD_Xu4iAxlFqcA3QdHi-L_yY63ENU1LTehh8-2d51f__nv58zPb799hNGhg"; const newAccessToken = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJidFZxNnRMWGpmcXdzbm5MR2FXMXdhdU9McDNiTmY4bWZ3Rm1SZ0lBS2VJIn0.eyJleHAiOjE3NDI5MTAzNzgsImlhdCI6MTc0MjkwOTQ3OCwianRpIjoiMTg4OTE3ZDYtNmM0OS00YWY0LTlhYjItZjEzYTM4MGY5OTgyIiwiaXNzIjoiaHR0cHM6Ly9wcmVwcm9kdWNjaW8ucGRzLmhlcy5jYXRzYWx1dC5nZW5jYXQuY2F0L2F1dGgvcmVhbG1zL0hFUyIsInN1YiI6Ijk1OGUyZWQ5LTJkNmUtNDZjOC1hOTE5LTU5MDQ0ZTYwMzVjMiIsInR5cCI6IkJlYXJlciIsImF6cCI6ImV0Yy1jY2YtcHJlIiwic2Vzc2lvbl9zdGF0ZSI6IjY5ODdhMzExLWM1NzQtNDc1Ni1hMzY0LWY1ZjQzZmViZjNhYiIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLWhlcyIsIlJPTEVfSEVTX0VUQyIsIlJPTEVfSEVTX0xBTkRJTkciXX0sInNjb3BlIjoiIiwic2lkIjoiNjk4N2EzMTEtYzU3NC00NzU2LWEzNjQtZjVmNDNmZWJmM2FiIiwiY2xpZW50SG9zdCI6IjEwLjUzLjI1NC4xNTAiLCJhY2Nlc3NfcnVsZXMiOnt9LCJhY2Nlc3NfaW5mbyI6eyJtb2R1bGVfY29kZSI6IkEwMDEiLCJyb2xlX3R5cGUiOiJOT1JNIiwidHJhY2VfdXNlcl9pZCI6IlVzZXJfSUQiLCJjZW50ZXJfdHlwZSI6IkUiLCJtcGlfcGF0aWVudF9pZCI6IjVlYzc0YzdiLTVhYTUtNGE4NC1iM2I0LWYwMmVkZGZkZjZkYyIsInVwX2NvZGUiOiIwNzczMyIsInRyYWNlX3VzZXJfZ2l2ZW5fbmFtZSI6IkdpdmVuIE5hbWUiLCJ0cmFjZV91c2VyX2ZhbWlseV9uYW1lIjoiRmFtaWx5IE5hbWUiLCJ1c2VyX3R5cGUiOiJBRE0iLCJjZW50ZXJfY29kZSI6IkUwODU4Njk2MyIsInByb2Zlc3Npb25hbF9jYXRlZ29yeSI6IjMwOTM0MzAwNiIsInNlcnZpY2VfY29kZSI6IjVTMDg5IiwiZXBfY29kZSI6IjAyMDgiLCJpZGVudGlmaWVyIjpbeyJ0eXBlIjoiRE5JIiwidmFsdWUiOiI3MzI4ODIxOUEifSx7InR5cGUiOiJOVU1DT0wiLCJ2YWx1ZSI6IjIifV0sImFsdF9pZGVudGlmaWVyIjpbeyJ0eXBlIjoiTVBJIiwidmFsdWUiOiIwNjIxY2Y3ZC03ZDYzLTQ5ZWMtODAwNi04YzA1NjkyZWQzNzcifV19LCJjbGllbnRBZGRyZXNzIjoiMTAuNTMuMjU0LjE1MCIsImNsaWVudF9pZCI6ImV0Yy1jY2YtcHJlIn0.zPxoN5d8_uxdbxeH7ly9sQdlnDxNp8_eWDzNSzjzkUPsRVkLxgjQw_WcjZfns-LNgGr0FToS6uSDIC7uj2hPpKqD7HPWbI6UPa7g-43Jf9rh-KSOXEHJWhlASi2w0n3xQHFSxHDYBwhyXlvWQ_Tr1Vh37MroWmEL84fdwqhcfZHmD9Ek52EuRRrB-XkymCmN1ZvO2GPU04LbBvQxLVzdAaKRzr1cUqHTnsEdgRduyKShPcyth01L_X7m128SIwjJsa1lYxFE_DbSDKz8dPTsxoS1DgwRxeArrkMlIdTHyxmDRezZUGhamyLuWpXLDXJCFfrTUB8-j_JnVlILHeR6O"; describe("HTTP Client", () => { let broker = createBroker(); let tokenManager: TokenManager; let axiosInstance: AxiosInstance; let axiosMockInstance: AxiosMockAdapter; beforeEach(() => { vi.restoreAllMocks(); // Reset all spies before each test vi.spyOn(window, "location", "get").mockReturnValue({ search: `?access_token=${access_token}&refresh_token=${refresh_token}`, } as unknown as Location); }); describe("Without validateMpid", () => { beforeEach(() => { tokenManager = createTokenManager(broker); axiosInstance = createAxiosInstance(tokenManager, broker); axiosMockInstance = new AxiosMockAdapter(axiosInstance); axiosMockInstance.reset(); }); it("should make a request with header", async () => { axiosMockInstance.onGet("/api/clinical-course").reply(200, "success"); // Perform the request const response = await axiosInstance.get("/api/clinical-course"); // Assertions expect(response.status).toBe(200); expect(response.data).toBe("success"); expect(axiosMockInstance.history.get?.length).toBe(1); // @ts-ignore expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe( `Bearer ${access_token}`, ); }); it("should retry request with new token if failed with 401 after token refresh, and requests after that have new token", async () => { axiosMockInstance .onGet("/api/clinical-course") .replyOnce(401, "Unauthorized") .onGet("/api/clinical-course") .replyOnce(200, "success") .onGet("/api/clinical-course/2") .replyOnce(200, "success"); vi.spyOn(axios, "post").mockResolvedValueOnce({ data: { access_token: newAccessToken, refresh_token: "new-refresh-token", }, }); // Perform the request const response = await axiosInstance.get("/api/clinical-course"); const secondResponse = await axiosInstance.get("/api/clinical-course/2"); // Assertions expect(response.status).toBe(200); expect(response.data).toBe("success"); expect(axiosMockInstance.history.get?.length).toBe(3); // 3 Calls 2 for the first request (request and retry), 1 for the second request expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe( "Bearer " + newAccessToken, ); expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe( "Bearer " + newAccessToken, ); }); it("should fail request if token refresh fails and publish an event", async () => { vi.spyOn(axios, "post").mockRejectedValueOnce({ response: { status: 400, statusText: "Bad Request" }, }); const brokerSpy = vi.spyOn(broker, "publish"); axiosMockInstance.onGet("/api/clinical-course").replyOnce(401, "Unauthorized"); await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow( "Request failed with status code 401", ); expect(axiosMockInstance.history.get?.length).toBe(1); // No retry happened expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {}); }); it("should fail request with error != 401 and should not retry", async () => { const refreshTokenSpy = vi.spyOn(tokenManager, "refreshToken"); const brokerSpy = vi.spyOn(broker, "publish"); axiosMockInstance.onGet("/api/clinical-course").replyOnce(500, "Internal Server Error"); await expect(axiosInstance.get("/api/clinical-course")).rejects.toMatchObject({ response: { status: 500 }, }); expect(axiosMockInstance.history.get?.length).toBe(1); expect(refreshTokenSpy).not.toHaveBeenCalled(); expect(brokerSpy).not.toHaveBeenCalled(); }); }); describe.skip("With validateMpid", () => { const validTokenHeaders = { "x-catsalut-mpid": "5ec74c7b-5aa5-4a84-b3b4-f02eddfdf6dc" }; beforeEach(() => { tokenManager = createTokenManager(broker); axiosInstance = createAxiosInstance(tokenManager, broker, true); axiosMockInstance = new AxiosMockAdapter(axiosInstance); axiosMockInstance.reset(); }); it("should throw error if mpid header is missing without sending broker event", async () => { axiosMockInstance.onGet("/api/clinical-course").reply(200, "success"); const brokerSpy = vi.spyOn(broker, "publish"); // Perform the request await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow( "Mpid header is missing", ); expect(brokerSpy).not.toHaveBeenCalled(); }); it("should throw error if mpid header value is invalid and send broker event ", async () => { axiosMockInstance.onGet("/api/clinical-course").reply(200, "success", { "x-catsalut-mpid": "invalid-mpid", }); const brokerSpy = vi.spyOn(broker, "publish"); // Perform the request await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow( "Mpid header value is invalid", ); expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.mpidHeaderInvalid, expect.any(Object)); }); it("should make a request with header and respond succesful if x-catsalut-mpid has valid value ", async () => { axiosMockInstance.onGet("/api/clinical-course").reply(200, "success", validTokenHeaders); // Perform the request const response = await axiosInstance.get("/api/clinical-course"); // Assertions expect(response.status).toBe(200); expect(response.data).toBe("success"); expect(axiosMockInstance.history.get?.length).toBe(1); // @ts-ignore expect(axiosMockInstance.history.get?.[0]?.headers.Authorization).toBe( `Bearer ${access_token}`, ); }); it("should retry request with new token if failed with 401 after token refresh, and requests after that have new token", async () => { axiosMockInstance .onGet("/api/clinical-course") .replyOnce(401, "Unauthorized") .onGet("/api/clinical-course") .replyOnce(200, "success", validTokenHeaders) .onGet("/api/clinical-course/2") .replyOnce(200, "success", validTokenHeaders); vi.spyOn(axios, "post").mockResolvedValueOnce({ data: { access_token: newAccessToken, refresh_token: "new-refresh-token", }, }); // Perform the request const response = await axiosInstance.get("/api/clinical-course"); const secondResponse = await axiosInstance.get("/api/clinical-course/2"); // Assertions expect(response.status).toBe(200); expect(response.data).toBe("success"); expect(axiosMockInstance.history.get?.length).toBe(3); // 3 Calls 2 for the first request (request and retry), 1 for the second request expect(axiosMockInstance.history.get?.[1]?.headers?.Authorization).toBe( "Bearer " + newAccessToken, ); expect(axiosMockInstance.history.get?.[2]?.headers?.Authorization).toBe( "Bearer " + newAccessToken, ); }); it("should fail request if token refresh fails and publish an event", async () => { vi.spyOn(axios, "post").mockRejectedValueOnce({ response: { status: 400, statusText: "Bad Request" }, }); const brokerSpy = vi.spyOn(broker, "publish"); axiosMockInstance.onGet("/api/clinical-course").replyOnce(401, "Unauthorized"); await expect(axiosInstance.get("/api/clinical-course")).rejects.toThrow( "Request failed with status code 401", ); expect(axiosMockInstance.history.get?.length).toBe(1); // No retry happened expect(brokerSpy).toHaveBeenCalledWith(BROKER_EVENTS.shell.refreshTokenFailed, {}); }); it("should fail request with error != 401 and should not retry", async () => { const refreshTokenSpy = vi.spyOn(tokenManager, "refreshToken"); const brokerSpy = vi.spyOn(broker, "publish"); axiosMockInstance.onGet("/api/clinical-course").replyOnce(500, "Internal Server Error"); await expect(axiosInstance.get("/api/clinical-course")).rejects.toMatchObject({ response: { status: 500 }, }); expect(axiosMockInstance.history.get?.length).toBe(1); expect(refreshTokenSpy).not.toHaveBeenCalled(); expect(brokerSpy).not.toHaveBeenCalled(); }); }); });