diff --git a/src/common/atlas/apiClient.ts b/src/common/atlas/apiClient.ts index 78bd688d..4cbd34d6 100644 --- a/src/common/atlas/apiClient.ts +++ b/src/common/atlas/apiClient.ts @@ -89,9 +89,8 @@ export class ApiClient { return !!(this.oauth2Client && this.accessToken); } - public async hasValidAccessToken(): Promise { - const accessToken = await this.getAccessToken(); - return accessToken !== undefined; + public async validateAccessToken(): Promise { + await this.getAccessToken(); } public async getIpInfo(): Promise<{ @@ -119,48 +118,57 @@ export class ApiClient { }>; } - async sendEvents(events: TelemetryEvent[]): Promise { - const headers: Record = { - Accept: "application/json", - "Content-Type": "application/json", - "User-Agent": this.options.userAgent, - }; - - const accessToken = await this.getAccessToken(); - if (accessToken) { - const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl); - headers["Authorization"] = `Bearer ${accessToken}`; + public async sendEvents(events: TelemetryEvent[]): Promise { + if (!this.options.credentials) { + await this.sendUnauthEvents(events); + return; + } - try { - const response = await fetch(authUrl, { - method: "POST", - headers, - body: JSON.stringify(events), - }); - - if (response.ok) { - return; + try { + await this.sendAuthEvents(events); + } catch (error) { + if (error instanceof ApiClientError) { + if (error.response.status !== 401) { + throw error; } + } - // If anything other than 401, throw the error - if (response.status !== 401) { - throw await ApiClientError.fromResponse(response); - } + // send unauth events if any of the following are true: + // 1: the token is not valid (not ApiClientError) + // 2: if the api responded with 401 (ApiClientError with status 401) + await this.sendUnauthEvents(events); + } + } - // For 401, fall through to unauthenticated endpoint - delete headers["Authorization"]; - } catch (error) { - // If the error is not a 401, rethrow it - if (!(error instanceof ApiClientError) || error.response.status !== 401) { - throw error; - } + private async sendAuthEvents(events: TelemetryEvent[]): Promise { + const accessToken = await this.getAccessToken(); + if (!accessToken) { + throw new Error("No access token available"); + } + const authUrl = new URL("api/private/v1.0/telemetry/events", this.options.baseUrl); + const response = await fetch(authUrl, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "User-Agent": this.options.userAgent, + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(events), + }); - // For 401 errors, fall through to unauthenticated endpoint - delete headers["Authorization"]; - } + if (!response.ok) { + throw await ApiClientError.fromResponse(response); } + } + + private async sendUnauthEvents(events: TelemetryEvent[]): Promise { + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + "User-Agent": this.options.userAgent, + }; - // Send to unauthenticated endpoint (either as fallback from 401 or direct if no token) const unauthUrl = new URL("api/private/unauth/telemetry/events", this.options.baseUrl); const response = await fetch(unauthUrl, { method: "POST", diff --git a/src/server.ts b/src/server.ts index 091ebd79..b0e8e19c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -188,7 +188,7 @@ export class Server { if (this.userConfig.apiClientId && this.userConfig.apiClientSecret) { try { - await this.session.apiClient.hasValidAccessToken(); + await this.session.apiClient.validateAccessToken(); } catch (error) { if (this.userConfig.connectionString === undefined) { console.error("Failed to validate MongoDB Atlas the credentials from the config: ", error); diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index fb79d08d..9d529376 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -61,7 +61,8 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati // Mock hasValidAccessToken for tests if (userConfig.apiClientId && userConfig.apiClientSecret) { const mockFn = jest.fn<() => Promise>().mockResolvedValue(true); - session.apiClient.hasValidAccessToken = mockFn; + // @ts-expect-error accessing private property for testing + session.apiClient.validateAccessToken = mockFn; } userConfig.telemetry = "disabled"; diff --git a/tests/unit/apiClient.test.ts b/tests/unit/apiClient.test.ts index a704e6b7..6b9fd427 100644 --- a/tests/unit/apiClient.test.ts +++ b/tests/unit/apiClient.test.ts @@ -95,7 +95,7 @@ describe("ApiClient", () => { }); describe("sendEvents", () => { - it("should send events to authenticated endpoint when token is available", async () => { + it("should send events to authenticated endpoint when token is available and valid", async () => { const mockFetch = jest.spyOn(global, "fetch"); mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); @@ -114,12 +114,33 @@ describe("ApiClient", () => { }); }); - it("should fall back to unauthenticated endpoint when token is not available", async () => { + it("should fall back to unauthenticated endpoint when token is not available via exception", async () => { const mockFetch = jest.spyOn(global, "fetch"); mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); // @ts-expect-error accessing private property for testing - apiClient.getAccessToken = jest.fn().mockResolvedValue(undefined); + apiClient.getAccessToken = jest.fn().mockRejectedValue(new Error("No access token available")); + + await apiClient.sendEvents(mockEvents); + + const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com"); + expect(mockFetch).toHaveBeenCalledWith(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + "User-Agent": "test-user-agent", + }, + body: JSON.stringify(mockEvents), + }); + }); + + it("should fall back to unauthenticated endpoint when token is undefined", async () => { + const mockFetch = jest.spyOn(global, "fetch"); + mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); + + // @ts-expect-error accessing private property for testing + apiClient.getAccessToken = jest.fn().mockReturnValueOnce(undefined); await apiClient.sendEvents(mockEvents);