From d7e56b26412469c0dcbd0ef8ada19c84bc82431e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Pretto?= <info@npretto.com> Date: Fri, 25 Oct 2024 14:13:46 +0200 Subject: [PATCH] test(sdk): add tests for auth flow (#49108) * test(sdk): add tests for auth flow * make the fetch token fn return something that will pass the validations we'll add in the next project * waitForRequest helper + add expects for card endpoints on api key test --- .../test/auth-flow.unit.spec.tsx | 158 ++++++++++++++++++ frontend/test/__support__/utils.ts | 27 ++- 2 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx diff --git a/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx b/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx new file mode 100644 index 00000000000..11d3b0f8a71 --- /dev/null +++ b/enterprise/frontend/src/embedding-sdk/test/auth-flow.unit.spec.tsx @@ -0,0 +1,158 @@ +import { render } from "@testing-library/react"; +import fetchMock from "fetch-mock"; + +import { + setupCardEndpoints, + setupCardQueryEndpoints, + setupCurrentUserEndpoint, + setupPropertiesEndpoints, +} from "__support__/server-mocks"; +import { waitForRequest } from "__support__/utils"; +import { + MetabaseProvider, + StaticQuestion, + defineEmbeddingSdkConfig, +} from "embedding-sdk/components/public"; +import type { SDKConfig } from "embedding-sdk/types"; +import { + createMockCard, + createMockSettings, + createMockTokenFeatures, + createMockUser, +} from "metabase-types/api/mocks"; + +const METABASE_INSTANCE_URL = "path:"; // "path:" is used by our core app support server mocks +const AUTH_PROVIDER_URL = "http://auth-provider:3000/sso/metabase"; +const MOCK_API_KEY = "mock-api-key"; +const USER_CURRENT_URL = `${METABASE_INSTANCE_URL}/api/user/current`; +const MOCK_SESSION = { + exp: 1729761473, + iat: 1729760873, + id: "5e03fb0a-2398-423f-98b3-0c4abaf0c47a", +}; + +const MOCK_CARD = createMockCard({ id: 1 }); + +const setup = (sdkConfig: SDKConfig) => { + return render( + <MetabaseProvider + config={{ + ...sdkConfig, + }} + > + <StaticQuestion questionId={1} /> + </MetabaseProvider>, + ); +}; + +const getLastUserApiCall = () => fetchMock.lastCall(USER_CURRENT_URL); +const getLastAuthProviderApiCall = () => fetchMock.lastCall(AUTH_PROVIDER_URL); +const getLastCardQueryApiCall = () => + fetchMock.lastCall(`${METABASE_INSTANCE_URL}/api/card/${MOCK_CARD.id}/query`); + +describe("SDK auth flow", () => { + beforeEach(() => { + fetchMock.reset(); + fetchMock.get(AUTH_PROVIDER_URL, { + status: 200, + body: MOCK_SESSION, + }); + + setupPropertiesEndpoints( + createMockSettings({ + "token-features": createMockTokenFeatures({ + embedding_sdk: true, + }), + }), + ); + + setupCurrentUserEndpoint(createMockUser({ id: 1 })); + + setupCardEndpoints(MOCK_CARD); + setupCardQueryEndpoints(MOCK_CARD, {} as any); + }); + + describe("when using jwtProvider", () => { + it("should retrieve the session from the jwtProvider and send it as 'X-Metabase-Session' header", async () => { + const sdkConfig = defineEmbeddingSdkConfig({ + metabaseInstanceUrl: METABASE_INSTANCE_URL, + jwtProviderUri: AUTH_PROVIDER_URL, + }); + + setup(sdkConfig); + + await waitForRequest(() => getLastAuthProviderApiCall()); + expect(getLastAuthProviderApiCall()![1]).toMatchObject({ + credentials: "include", + method: "GET", + }); + + await waitForRequest(() => getLastUserApiCall()); + expect(getLastUserApiCall()![1]).toMatchObject({ + headers: { "X-Metabase-Session": [MOCK_SESSION.id] }, + }); + + await waitForRequest(() => getLastCardQueryApiCall()); + expect(getLastCardQueryApiCall()![1]).toMatchObject({ + headers: { "X-Metabase-Session": [MOCK_SESSION.id] }, + }); + }); + + it("should use `fetchRequestToken` if provided", async () => { + const customFetchFunction = jest.fn().mockImplementation(() => ({ + ...MOCK_SESSION, + id: "mock-id-from-custom-fetch-function", + })); + + const sdkConfig = defineEmbeddingSdkConfig({ + metabaseInstanceUrl: METABASE_INSTANCE_URL, + jwtProviderUri: AUTH_PROVIDER_URL, + fetchRequestToken: customFetchFunction, + }); + + setup(sdkConfig); + + expect(customFetchFunction).toHaveBeenCalledWith(AUTH_PROVIDER_URL); + + await waitForRequest(() => getLastUserApiCall()); + expect(getLastUserApiCall()![1]).toMatchObject({ + headers: { + "X-Metabase-Session": ["mock-id-from-custom-fetch-function"], + }, + }); + + await waitForRequest(() => getLastCardQueryApiCall()); + expect(getLastCardQueryApiCall()![1]).toMatchObject({ + headers: { + "X-Metabase-Session": ["mock-id-from-custom-fetch-function"], + }, + }); + }); + }); + + describe("when using apiKeyProvider", () => { + it("should send the api key as 'X-Api-Key' header", async () => { + const sdkConfig = defineEmbeddingSdkConfig({ + metabaseInstanceUrl: METABASE_INSTANCE_URL, + apiKey: MOCK_API_KEY, + }); + + setup(sdkConfig); + + await waitForRequest(() => getLastUserApiCall()); + expect(getLastUserApiCall()![1]).toMatchObject({ + headers: { "X-Api-Key": [MOCK_API_KEY] }, + }); + + await waitForRequest(() => getLastCardQueryApiCall()); + expect(getLastCardQueryApiCall()![1]).toMatchObject({ + headers: { "X-Api-Key": [MOCK_API_KEY] }, + }); + + await waitForRequest(() => getLastCardQueryApiCall()); + expect(getLastCardQueryApiCall()![1]).toMatchObject({ + headers: { "X-Api-Key": [MOCK_API_KEY] }, + }); + }); + }); +}); diff --git a/frontend/test/__support__/utils.ts b/frontend/test/__support__/utils.ts index aee8a258683..9222d9a5e4e 100644 --- a/frontend/test/__support__/utils.ts +++ b/frontend/test/__support__/utils.ts @@ -1,4 +1,6 @@ -import { act } from "./ui"; +import type fetchMock from "fetch-mock"; + +import { act, waitFor } from "./ui"; export const getNextId = (() => { let id = 0; @@ -10,3 +12,26 @@ export async function delay(duration: number) { await new Promise(resolve => setTimeout(resolve, duration)); }); } + +/** Waits for a request to have been made. It's useful to wait for a request and + * then check its details separately, to make tests less flaky and to have + * better failure messages (request arrived but wrong details vs request never + * arrived) + */ +export const waitForRequest = async ( + requestFn: () => fetchMock.MockCall | undefined, +) => { + try { + // try catch to make jest show the line where waitForRequest was originally called + await waitFor(() => { + if (!requestFn()) { + throw new Error("Request not found"); + } + }); + } catch (error) { + if (error instanceof Error) { + Error.captureStackTrace(error, waitForRequest); + } + throw error; + } +}; -- GitLab