From 50f504f96b9bd8a81e6a6b7a5e598a8c49e4b64b Mon Sep 17 00:00:00 2001 From: Todd Martin Date: Sun, 11 Jan 2026 11:42:28 -0500 Subject: [PATCH] Remove duplicate code from merge. --- libs/common/src/abstractions/api.service.ts | 7 - libs/common/src/services/api.service.spec.ts | 794 ------------------- 2 files changed, 801 deletions(-) diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 49a6afdf5e0..7e4ff031ef2 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -446,13 +446,6 @@ export abstract class ApiService { abstract postBitPayInvoice(request: BitPayInvoiceRequest): Promise; abstract postSetupPayment(): Promise; - /** - * Retrieves the bearer access token for the user. - * If the access token is expired or within 5 minutes of expiration, attemps to refresh the token - * and persists the refresh token to state before returning it. - * @param userId The user for whom we're retrieving the access token - * @returns The access token, or an Error if no access token exists. - */ /** * Retrieves the bearer access token for the user. * If the access token is expired or within 5 minutes of expiration, attempts to refresh the token diff --git a/libs/common/src/services/api.service.spec.ts b/libs/common/src/services/api.service.spec.ts index 1a7ddb9d30b..b5e72dfd899 100644 --- a/libs/common/src/services/api.service.spec.ts +++ b/libs/common/src/services/api.service.spec.ts @@ -1245,798 +1245,4 @@ describe("ApiService", () => { expect(logoutCallback).not.toHaveBeenCalled(); }); }); - - describe("When a 401 Unauthorized status is received", () => { - it("retries request with refreshed token when initial request with access token returns 401", async () => { - // This test verifies the 401 retry flow: - // 1. Initial request with valid token returns 401 (token expired server-side) - // 2. After 401, buildRequest is called again, which checks tokenNeedsRefresh - // 3. tokenNeedsRefresh returns true, triggering refreshToken via getActiveBearerToken - // 4. refreshToken makes an HTTP call to /connect/token to get new tokens - // 5. setTokens is called to store the new tokens, returning the refreshed access token - // 6. Request is retried with the refreshed token and succeeds - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("access_token"); - // First call (initial request): token doesn't need refresh yet - // Subsequent calls (after 401): token needs refresh, triggering the refresh flow - tokenService.tokenNeedsRefresh - .calledWith(testActiveUser) - .mockResolvedValueOnce(false) - .mockResolvedValue(true); - - tokenService.getRefreshToken.calledWith(testActiveUser).mockResolvedValue("refresh_token"); - - tokenService.decodeAccessToken - .calledWith(testActiveUser) - .mockResolvedValue({ client_id: "web" }); - - tokenService.decodeAccessToken - .calledWith("new_access_token") - .mockResolvedValue({ sub: testActiveUser }); - - vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutAction.Lock)); - - vaultTimeoutSettingsService.getVaultTimeoutByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutStringType.Never)); - - tokenService.setTokens - .calledWith( - "new_access_token", - VaultTimeoutAction.Lock, - VaultTimeoutStringType.Never, - "new_refresh_token", - ) - .mockResolvedValue({ accessToken: "new_access_token" }); - - const nativeFetch = jest.fn, [request: Request]>(); - let callCount = 0; - - nativeFetch.mockImplementation((request) => { - callCount++; - - // First call: initial request with expired token returns 401 - if (callCount === 1) { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - // Second call: token refresh request - if (callCount === 2 && request.url.includes("identity")) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - access_token: "new_access_token", - token_type: "Bearer", - refresh_token: "new_refresh_token", - }), - } satisfies Partial as Response); - } - - // Third call: retry with refreshed token succeeds - if (callCount === 3) { - expect(request.headers.get("Authorization")).toBe("Bearer new_access_token"); - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ data: "success" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - throw new Error(`Unexpected call #${callCount}: ${request.method} ${request.url}`); - }); - - sut.nativeFetch = nativeFetch; - - const response = await sut.send("GET", "/something", null, true, true, null, null); - - expect(nativeFetch).toHaveBeenCalledTimes(3); - expect(response).toEqual({ data: "success" }); - }); - - it("does not retry when request has no access token and returns 401", async () => { - environmentService.environment$ = of({ - getApiUrl: () => "https://example.com", - } satisfies Partial as Environment); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, false, true, null, null), - ).rejects.toMatchObject({ message: "Unauthorized" }); - - // Should only be called once (no retry) - expect(nativeFetch).toHaveBeenCalledTimes(1); - }); - - it("does not retry when request returns non-401 error", async () => { - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("valid_token"); - tokenService.tokenNeedsRefresh.calledWith(testActiveUser).mockResolvedValue(false); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 400, - json: () => Promise.resolve({ message: "Bad Request" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, true, true, null, null), - ).rejects.toMatchObject({ message: "Bad Request" }); - - // Should only be called once (no retry for non-401 errors) - expect(nativeFetch).toHaveBeenCalledTimes(1); - }); - - it("does not attempt to log out unauthenticated user", async () => { - environmentService.environment$ = of({ - getApiUrl: () => "https://example.com", - } satisfies Partial as Environment); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, false, true, null, null), - ).rejects.toMatchObject({ message: "Unauthorized" }); - - expect(logoutCallback).not.toHaveBeenCalled(); - }); - - it("does not retry when hasResponse is false", async () => { - environmentService.environment$ = of({ - getApiUrl: () => "https://example.com", - } satisfies Partial as Environment); - - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("expired_token"); - tokenService.tokenNeedsRefresh.calledWith(testActiveUser).mockResolvedValue(false); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - // When hasResponse is false, the method should throw even though no retry happens - await expect( - async () => await sut.send("POST", "/something", null, true, false, null, null), - ).rejects.toMatchObject({ message: "Unauthorized" }); - - // Should only be called once (no retry when hasResponse is false) - expect(nativeFetch).toHaveBeenCalledTimes(1); - }); - - it("uses original user token for retry even if active user changes between requests", async () => { - // Setup: Initial request is for testActiveUser, but during the retry, the active user switches - // to testInactiveUser. The retry should still use testActiveUser's refreshed token. - - let activeUserId = testActiveUser; - - // Mock accountService to return different active users based on when it's called - accountService.activeAccount$ = of({ - id: activeUserId, - email: "user1@example.com", - emailVerified: true, - name: "Test Name", - } satisfies ObservedValueOf); - - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - environmentService.getEnvironment$.calledWith(testInactiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://inactive.example.com", - getIdentityUrl: () => "https://identity.inactive.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken - .calledWith(testActiveUser) - .mockResolvedValue("active_access_token"); - tokenService.tokenNeedsRefresh - .calledWith(testActiveUser) - .mockResolvedValueOnce(false) - .mockResolvedValue(true); - - tokenService.getRefreshToken - .calledWith(testActiveUser) - .mockResolvedValue("active_refresh_token"); - - tokenService.decodeAccessToken - .calledWith(testActiveUser) - .mockResolvedValue({ client_id: "web" }); - - tokenService.decodeAccessToken - .calledWith("active_new_access_token") - .mockResolvedValue({ sub: testActiveUser }); - - vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutAction.Lock)); - - vaultTimeoutSettingsService.getVaultTimeoutByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutStringType.Never)); - - tokenService.setTokens - .calledWith( - "active_new_access_token", - VaultTimeoutAction.Lock, - VaultTimeoutStringType.Never, - "active_new_refresh_token", - ) - .mockResolvedValue({ accessToken: "active_new_access_token" }); - - // Mock tokens for inactive user (should NOT be used) - tokenService.getAccessToken - .calledWith(testInactiveUser) - .mockResolvedValue("inactive_access_token"); - - const nativeFetch = jest.fn, [request: Request]>(); - let callCount = 0; - - nativeFetch.mockImplementation((request) => { - callCount++; - - // First call: initial request with active user's token returns 401 - if (callCount === 1) { - expect(request.url).toBe("https://example.com/something"); - expect(request.headers.get("Authorization")).toBe("Bearer active_access_token"); - - // After the 401, simulate active user changing - activeUserId = testInactiveUser; - accountService.activeAccount$ = of({ - id: testInactiveUser, - email: "user2@example.com", - emailVerified: true, - name: "Inactive User", - } satisfies ObservedValueOf); - - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - // Second call: token refresh request for ORIGINAL user (testActiveUser) - if (callCount === 2 && request.url.includes("identity")) { - expect(request.url).toContain("identity.example.com"); - return Promise.resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - access_token: "active_new_access_token", - token_type: "Bearer", - refresh_token: "active_new_refresh_token", - }), - } satisfies Partial as Response); - } - - // Third call: retry with ORIGINAL user's refreshed token, NOT the new active user's token - if (callCount === 3) { - expect(request.url).toBe("https://example.com/something"); - expect(request.headers.get("Authorization")).toBe("Bearer active_new_access_token"); - // Verify we're NOT using the inactive user's endpoint - expect(request.url).not.toContain("inactive"); - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ data: "success with original user" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - throw new Error(`Unexpected call #${callCount}: ${request.method} ${request.url}`); - }); - - sut.nativeFetch = nativeFetch; - - // Explicitly pass testActiveUser to ensure the request is for that specific user - const response = await sut.send("GET", "/something", null, testActiveUser, true, null, null); - - expect(nativeFetch).toHaveBeenCalledTimes(3); - expect(response).toEqual({ data: "success with original user" }); - - // Verify that inactive user's token was never requested - expect(tokenService.getAccessToken.calledWith(testInactiveUser)).not.toHaveBeenCalled(); - }); - - it("throws error when retry also returns 401", async () => { - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("access_token"); - // First call (initial request): token doesn't need refresh yet - // Subsequent calls (after 401): token needs refresh, triggering the refresh flow - tokenService.tokenNeedsRefresh - .calledWith(testActiveUser) - .mockResolvedValueOnce(false) - .mockResolvedValue(true); - - tokenService.getRefreshToken.calledWith(testActiveUser).mockResolvedValue("refresh_token"); - - tokenService.decodeAccessToken - .calledWith(testActiveUser) - .mockResolvedValue({ client_id: "web" }); - - tokenService.decodeAccessToken - .calledWith("new_access_token") - .mockResolvedValue({ sub: testActiveUser }); - - vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutAction.Lock)); - - vaultTimeoutSettingsService.getVaultTimeoutByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutStringType.Never)); - - tokenService.setTokens - .calledWith( - "new_access_token", - VaultTimeoutAction.Lock, - VaultTimeoutStringType.Never, - "new_refresh_token", - ) - .mockResolvedValue({ accessToken: "new_access_token" }); - - const nativeFetch = jest.fn, [request: Request]>(); - let callCount = 0; - - nativeFetch.mockImplementation((request) => { - callCount++; - - // First call: initial request with expired token returns 401 - if (callCount === 1) { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - // Second call: token refresh request - if (callCount === 2 && request.url.includes("identity")) { - return Promise.resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - access_token: "new_access_token", - token_type: "Bearer", - refresh_token: "new_refresh_token", - }), - } satisfies Partial as Response); - } - - // Third call: retry with refreshed token still returns 401 (user no longer has permission) - if (callCount === 3) { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Still Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - throw new Error("Unexpected call"); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, true, true, null, null), - ).rejects.toMatchObject({ message: "Still Unauthorized" }); - - expect(nativeFetch).toHaveBeenCalledTimes(3); - expect(logoutCallback).toHaveBeenCalledWith("invalidAccessToken"); - }); - - it("handles concurrent requests that both receive 401 and share token refresh", async () => { - // This test verifies the race condition scenario: - // 1. Request A starts with valid token - // 2. Request B starts with valid token - // 3. Request A gets 401, triggers refresh - // 4. Request B gets 401 while A is refreshing - // 5. Request B should wait for A's refresh to complete (via refreshTokenPromise cache) - // 6. Both requests retry with the new token - - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - getIdentityUrl: () => "https://identity.example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("expired_token"); - - // First two calls: token doesn't need refresh yet - // Subsequent calls: token needs refresh - tokenService.tokenNeedsRefresh - .calledWith(testActiveUser) - .mockResolvedValueOnce(false) // Request A initial - .mockResolvedValueOnce(false) // Request B initial - .mockResolvedValue(true); // Both retries after 401 - - tokenService.getRefreshToken.calledWith(testActiveUser).mockResolvedValue("refresh_token"); - - tokenService.decodeAccessToken - .calledWith(testActiveUser) - .mockResolvedValue({ client_id: "web" }); - - tokenService.decodeAccessToken - .calledWith("new_access_token") - .mockResolvedValue({ sub: testActiveUser }); - - vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutAction.Lock)); - - vaultTimeoutSettingsService.getVaultTimeoutByUserId$ - .calledWith(testActiveUser) - .mockReturnValue(of(VaultTimeoutStringType.Never)); - - tokenService.setTokens - .calledWith( - "new_access_token", - VaultTimeoutAction.Lock, - VaultTimeoutStringType.Never, - "new_refresh_token", - ) - .mockResolvedValue({ accessToken: "new_access_token" }); - - const nativeFetch = jest.fn, [request: Request]>(); - let apiRequestCount = 0; - let refreshRequestCount = 0; - - nativeFetch.mockImplementation((request) => { - if (request.url.includes("identity")) { - refreshRequestCount++; - // Simulate slow token refresh to expose race condition - return new Promise((resolve) => - setTimeout( - () => - resolve({ - ok: true, - status: 200, - json: () => - Promise.resolve({ - access_token: "new_access_token", - token_type: "Bearer", - refresh_token: "new_refresh_token", - }), - } satisfies Partial as Response), - 100, - ), - ); - } - - apiRequestCount++; - const currentCall = apiRequestCount; - - // First two calls (Request A and B initial attempts): both return 401 - if (currentCall === 1 || currentCall === 2) { - return Promise.resolve({ - ok: false, - status: 401, - json: () => Promise.resolve({ message: "Unauthorized" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - // Third and fourth calls (retries after refresh): both succeed - if (currentCall === 3 || currentCall === 4) { - expect(request.headers.get("Authorization")).toBe("Bearer new_access_token"); - return Promise.resolve({ - ok: true, - status: 200, - json: () => Promise.resolve({ data: `success-${currentCall}` }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - } - - throw new Error(`Unexpected API call #${currentCall}: ${request.method} ${request.url}`); - }); - - sut.nativeFetch = nativeFetch; - - // Make two concurrent requests - const [responseA, responseB] = await Promise.all([ - sut.send("GET", "/endpoint-a", null, testActiveUser, true, null, null), - sut.send("GET", "/endpoint-b", null, testActiveUser, true, null, null), - ]); - - // Both requests should succeed - expect(responseA).toMatchObject({ data: expect.stringContaining("success") }); - expect(responseB).toMatchObject({ data: expect.stringContaining("success") }); - - // Verify only ONE token refresh was made (they shared the refresh) - expect(refreshRequestCount).toBe(1); - - // Verify the total number of API requests: 2 initial + 2 retries = 4 - expect(apiRequestCount).toBe(4); - - // Verify setTokens was only called once - expect(tokenService.setTokens).toHaveBeenCalledTimes(1); - }); - }); - - describe("When 403 Forbidden response is received from API request", () => { - it("logs out the authenticated user", async () => { - environmentService.getEnvironment$.calledWith(testActiveUser).mockReturnValue( - of({ - getApiUrl: () => "https://example.com", - } satisfies Partial as Environment), - ); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - tokenService.getAccessToken.calledWith(testActiveUser).mockResolvedValue("valid_token"); - tokenService.tokenNeedsRefresh.calledWith(testActiveUser).mockResolvedValue(false); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 403, - json: () => Promise.resolve({ message: "Forbidden" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, true, true, null, null), - ).rejects.toMatchObject({ message: "Forbidden" }); - - expect(logoutCallback).toHaveBeenCalledWith("invalidAccessToken"); - }); - - it("does not attempt to log out unauthenticated user", async () => { - environmentService.environment$ = of({ - getApiUrl: () => "https://example.com", - } satisfies Partial as Environment); - - httpOperations.createRequest.mockImplementation((url, request) => { - return { - url: url, - cache: request.cache, - credentials: request.credentials, - method: request.method, - mode: request.mode, - signal: request.signal, - headers: new Headers(request.headers), - } satisfies Partial as unknown as Request; - }); - - const nativeFetch = jest.fn, [request: Request]>(); - - nativeFetch.mockImplementation((request) => { - return Promise.resolve({ - ok: false, - status: 403, - json: () => Promise.resolve({ message: "Forbidden" }), - headers: new Headers({ - "content-type": "application/json", - }), - } satisfies Partial as Response); - }); - - sut.nativeFetch = nativeFetch; - - await expect( - async () => await sut.send("GET", "/something", null, false, true, null, null), - ).rejects.toMatchObject({ message: "Forbidden" }); - - expect(logoutCallback).not.toHaveBeenCalled(); - }); - }); });