import { combineReducers } from "@reduxjs/toolkit"; import { waitForElementToBeRemoved } from "@testing-library/react"; import { Route } from "react-router"; import userEvent from "@testing-library/user-event"; import fetchMock from "fetch-mock"; import { renderWithProviders, screen, waitFor } from "__support__/ui"; import { ImpersonationModal } from "metabase-enterprise/advanced_permissions/components/ImpersonationModal/ImpersonationModal"; import { shared } from "metabase-enterprise/shared/reducer"; import { advancedPermissionsSlice } from "metabase-enterprise/advanced_permissions/reducer"; import { setupDatabaseEndpoints, setupUserAttributesEndpoint, } from "__support__/server-mocks"; import { createMockDatabase, createMockTable } from "metabase-types/api/mocks"; import { setupExistingImpersonationEndpoint, setupMissingImpersonationEndpoint, } from "__support__/server-mocks/impersonation"; import { createMockImpersonation } from "metabase-types/api/mocks/permissions"; import { getImpersonations } from "metabase-enterprise/advanced_permissions/selectors"; import { AdvancedPermissionsStoreState } from "metabase-enterprise/advanced_permissions/types"; const groupId = 2; const databaseId = 1; const selectedAttribute = "foo"; const defaultUserAttributes = ["foo", "bar"]; const setup = async ({ userAttributes = defaultUserAttributes, hasImpersonation = true, } = {}) => { const database = createMockDatabase({ id: databaseId, tables: [createMockTable()], }); setupDatabaseEndpoints(database); fetchMock.get( { url: `path:/api/database/${databaseId}/metadata`, query: { include_hidden: true }, }, database, ); setupUserAttributesEndpoint(userAttributes); if (hasImpersonation) { setupExistingImpersonationEndpoint( createMockImpersonation({ db_id: databaseId, group_id: groupId, attribute: selectedAttribute, }), ); } else { setupMissingImpersonationEndpoint(databaseId, groupId); } const { store } = renderWithProviders( <> <Route path="/" /> <Route path="database/:databaseId/impersonated/group/:groupId" component={ImpersonationModal} /> </>, { initialRoute: `database/${databaseId}/impersonated/group/${groupId}`, withRouter: true, customReducers: { plugins: combineReducers({ shared: shared.reducer, advancedPermissionsPlugin: advancedPermissionsSlice.reducer, }), }, }, ); await waitForElementToBeRemoved(() => screen.queryByText("Loading...")); return store; }; describe("impersonation modal", () => { it("should render the content", async () => { await setup(); expect( await screen.findByText("Map a user attribute to database roles"), ).toBeInTheDocument(); expect( await screen.findByText( "When the person runs a query (including native queries), Metabase will impersonate the privileges of the database role you associate with the user attribute.", ), ).toBeInTheDocument(); expect( await screen.findByRole("link", { name: /learn more/i }), ).toHaveAttribute( "href", "https://www.metabase.com/docs/latest/permissions/data.html", ); expect( await screen.findByText( "Make sure the main database credential has access to everything different user groups may need access to. It's what Metabase uses to sync table information.", ), ).toBeInTheDocument(); expect( await screen.findByRole("link", { name: /edit settings/i }), ).toHaveAttribute("href", "/admin/databases/1"); }); it("should not update impersonation if it has not changed", async () => { const store = await setup({ userAttributes: ["foo"] }); userEvent.click(screen.getByText(/save/i)); expect( getImpersonations(store.getState() as AdvancedPermissionsStoreState), ).toHaveLength(0); }); it("should create impersonation", async () => { const store = await setup({ hasImpersonation: false }); userEvent.click(await screen.findByText(/pick a user attribute/i)); userEvent.click(await screen.findByText("foo")); expect(await screen.findByRole("button", { name: /save/i })).toBeEnabled(); userEvent.click(await screen.findByRole("button", { name: /save/i })); await waitFor(() => { expect( getImpersonations(store.getState() as AdvancedPermissionsStoreState), ).toStrictEqual([ { attribute: "foo", db_id: 1, group_id: 2, }, ]); }); }); it("should update impersonation", async () => { const store = await setup(); userEvent.click(await screen.findByText(selectedAttribute)); userEvent.click(await screen.findByText("bar")); expect(await screen.findByRole("button", { name: /save/i })).toBeEnabled(); userEvent.click(await screen.findByRole("button", { name: /save/i })); await waitFor(() => { expect( getImpersonations(store.getState() as AdvancedPermissionsStoreState), ).toStrictEqual([ { attribute: "bar", db_id: 1, group_id: 2, }, ]); }); }); it("should show only already selected attribute if attributes array is empty", async () => { await setup({ hasImpersonation: true, userAttributes: [] }); await screen.findByText(selectedAttribute); expect(await screen.findByRole("button", { name: /save/i })).toBeEnabled(); }); it("should show the link to people settings if there is no impersonation and no attributes", async () => { await setup({ hasImpersonation: false, userAttributes: [] }); expect( await screen.findByText( "To associate a user with a database role, you'll need to give that user at least one user attribute.", ), ).toBeInTheDocument(); expect( await screen.findByRole("link", { name: /edit user settings/i }), ).toHaveAttribute("href", "/admin/people"); expect(await screen.findByRole("button", { name: /close/i })).toBeEnabled(); }); });