diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index fef85ab87..0510a7c23 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -275,8 +275,7 @@ export interface DataMaskingPolicy { startPosition: number; length: number; }>; - excludedPaths: string[]; - isPolicyEnabled: boolean; + excludedPaths?: string[]; } export interface MaterializedView { diff --git a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx index 7dfd49b11..ff7485b5e 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.test.tsx @@ -30,7 +30,6 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({ dataMaskingPolicy: { includedPaths: [], excludedPaths: ["/excludedPath"], - isPolicyEnabled: true, }, indexes: [], }), @@ -307,12 +306,10 @@ describe("SettingsComponent", () => { dataMaskingContent: { includedPaths: [], excludedPaths: ["/excludedPath"], - isPolicyEnabled: true, }, dataMaskingContentBaseline: { includedPaths: [], excludedPaths: [], - isPolicyEnabled: false, }, isDataMaskingDirty: true, }); @@ -326,7 +323,6 @@ describe("SettingsComponent", () => { expect(wrapper.state("dataMaskingContentBaseline")).toEqual({ includedPaths: [], excludedPaths: ["/excludedPath"], - isPolicyEnabled: true, }); }); @@ -340,7 +336,6 @@ describe("SettingsComponent", () => { const invalidPolicy: InvalidPolicy = { includedPaths: "invalid", excludedPaths: [], - isPolicyEnabled: false, }; // Use type assertion since we're deliberately testing with invalid data settingsComponentInstance["onDataMaskingContentChange"](invalidPolicy as unknown as DataModels.DataMaskingPolicy); @@ -349,7 +344,6 @@ describe("SettingsComponent", () => { expect(wrapper.state("dataMaskingContent")).toEqual({ includedPaths: "invalid", excludedPaths: [], - isPolicyEnabled: false, }); expect(wrapper.state("dataMaskingValidationErrors")).toEqual(["includedPaths must be an array"]); @@ -364,7 +358,6 @@ describe("SettingsComponent", () => { }, ], excludedPaths: ["/excludedPath"], - isPolicyEnabled: true, }; settingsComponentInstance["onDataMaskingContentChange"](validPolicy); @@ -388,7 +381,6 @@ describe("SettingsComponent", () => { }, ], excludedPaths: ["/excludedPath1"], - isPolicyEnabled: false, }; const modifiedPolicy = { @@ -401,7 +393,6 @@ describe("SettingsComponent", () => { }, ], excludedPaths: ["/excludedPath2"], - isPolicyEnabled: true, }; // Set initial state diff --git a/src/Explorer/Controls/Settings/SettingsComponent.tsx b/src/Explorer/Controls/Settings/SettingsComponent.tsx index 95f7159cc..9f50b53c2 100644 --- a/src/Explorer/Controls/Settings/SettingsComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsComponent.tsx @@ -16,7 +16,7 @@ import { import { useIndexingPolicyStore } from "Explorer/Tabs/QueryTab/ResultsView"; import { useDatabases } from "Explorer/useDatabases"; import { isFabricNative } from "Platform/Fabric/FabricUtil"; -import { isCapabilityEnabled, isVectorSearchEnabled } from "Utils/CapabilityUtils"; +import { isVectorSearchEnabled } from "Utils/CapabilityUtils"; import { isRunningOnPublicCloud } from "Utils/CloudUtils"; import * as React from "react"; import DiscardIcon from "../../../../images/discard.svg"; @@ -70,6 +70,7 @@ import { getMongoNotification, getTabTitle, hasDatabaseSharedThroughput, + isDataMaskingEnabled, isDirty, parseConflictResolutionMode, parseConflictResolutionProcedure, @@ -686,22 +687,14 @@ export class SettingsComponent extends React.Component { - if (!newDataMasking.excludedPaths) { - newDataMasking.excludedPaths = []; - } - if (!newDataMasking.includedPaths) { - newDataMasking.includedPaths = []; - } - const validationErrors = []; - if (!Array.isArray(newDataMasking.includedPaths)) { + if (newDataMasking.includedPaths === undefined || newDataMasking.includedPaths === null) { + validationErrors.push("includedPaths is required"); + } else if (!Array.isArray(newDataMasking.includedPaths)) { validationErrors.push("includedPaths must be an array"); } - if (!Array.isArray(newDataMasking.excludedPaths)) { - validationErrors.push("excludedPaths must be an array"); - } - if (typeof newDataMasking.isPolicyEnabled !== "boolean") { - validationErrors.push("isPolicyEnabled must be a boolean"); + if (newDataMasking.excludedPaths !== undefined && !Array.isArray(newDataMasking.excludedPaths)) { + validationErrors.push("excludedPaths must be an array if provided"); } this.setState({ @@ -842,7 +835,6 @@ export class SettingsComponent extends React.Component { - const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking); - const isSqlAccount = userContext.apiType === "SQL"; - - return isSqlAccount && hasDataMaskingCapability; // Only show for SQL accounts with DDM capability - }; - - if (shouldEnableDDM()) { + if (isDataMaskingEnabled(this.collection.dataMaskingPolicy?.())) { const dataMaskingComponentProps: DataMaskingComponentProps = { shouldDiscardDataMasking: this.state.shouldDiscardDataMasking, resetShouldDiscardDataMasking: this.resetShouldDiscardDataMasking, diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.test.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.test.tsx index a51d55a32..4e25c1980 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.test.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.test.tsx @@ -53,7 +53,6 @@ describe("DataMaskingComponent", () => { }, ], excludedPaths: [], - isPolicyEnabled: false, }; let changeContentCallback: () => void; @@ -78,7 +77,7 @@ describe("DataMaskingComponent", () => { , ); @@ -123,7 +122,7 @@ describe("DataMaskingComponent", () => { }); it("resets content when shouldDiscardDataMasking is true", async () => { - const baselinePolicy = { ...samplePolicy, isPolicyEnabled: true }; + const baselinePolicy = { ...samplePolicy, excludedPaths: ["/excluded"] }; const wrapper = mount( { wrapper.update(); // Update baseline to trigger componentDidUpdate - const newBaseline = { ...samplePolicy, isPolicyEnabled: true }; + const newBaseline = { ...samplePolicy, excludedPaths: ["/excluded"] }; wrapper.setProps({ dataMaskingContentBaseline: newBaseline }); expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true); @@ -174,7 +173,6 @@ describe("DataMaskingComponent", () => { const invalidPolicy: Record = { includedPaths: "not an array", excludedPaths: [] as string[], - isPolicyEnabled: "not a boolean", }; mockGetValue.mockReturnValue(JSON.stringify(invalidPolicy)); @@ -197,7 +195,7 @@ describe("DataMaskingComponent", () => { wrapper.update(); // First change - const modifiedPolicy1 = { ...samplePolicy, isPolicyEnabled: true }; + const modifiedPolicy1 = { ...samplePolicy, excludedPaths: ["/path1"] }; mockGetValue.mockReturnValue(JSON.stringify(modifiedPolicy1)); changeContentCallback(); expect(mockProps.onDataMaskingDirtyChange).toHaveBeenCalledWith(true); diff --git a/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.tsx b/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.tsx index 80314fe7c..61ac40931 100644 --- a/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.tsx +++ b/src/Explorer/Controls/Settings/SettingsSubComponents/DataMaskingComponent.tsx @@ -1,12 +1,10 @@ import { MessageBar, MessageBarType, Stack } from "@fluentui/react"; import * as monaco from "monaco-editor"; import * as React from "react"; -import * as Constants from "../../../../Common/Constants"; import * as DataModels from "../../../../Contracts/DataModels"; -import { isCapabilityEnabled } from "../../../../Utils/CapabilityUtils"; import { loadMonaco } from "../../../LazyMonaco"; import { titleAndInputStackProps, unsavedEditorWarningMessage } from "../SettingsRenderUtils"; -import { isDirty as isContentDirty } from "../SettingsUtils"; +import { isDirty as isContentDirty, isDataMaskingEnabled } from "../SettingsUtils"; export interface DataMaskingComponentProps { shouldDiscardDataMasking: boolean; @@ -24,16 +22,8 @@ interface DataMaskingComponentState { } const emptyDataMaskingPolicy: DataModels.DataMaskingPolicy = { - includedPaths: [ - { - path: "/", - strategy: "Default", - startPosition: 0, - length: -1, - }, - ], + includedPaths: [], excludedPaths: [], - isPolicyEnabled: true, }; export class DataMaskingComponent extends React.Component { @@ -140,7 +130,7 @@ export class DataMaskingComponent extends React.Component { + const isSqlAccount = userContext.apiType === "SQL"; + if (!isSqlAccount) { + return false; + } + + const hasDataMaskingCapability = isCapabilityEnabled(Constants.CapabilityNames.EnableDynamicDataMasking); + const hasDataMaskingPolicyFromCollection = + dataMaskingPolicy?.includedPaths?.length > 0 || dataMaskingPolicy?.excludedPaths?.length > 0; + + return hasDataMaskingCapability || hasDataMaskingPolicyFromCollection; +}; + export const parseConflictResolutionMode = (modeFromBackend: string): DataModels.ConflictResolutionMode => { // Backend can contain different casing as it does case-insensitive comparisson if (!modeFromBackend) { diff --git a/src/Explorer/Controls/Settings/TestUtils.tsx b/src/Explorer/Controls/Settings/TestUtils.tsx index f30e84709..c3b3f8b84 100644 --- a/src/Explorer/Controls/Settings/TestUtils.tsx +++ b/src/Explorer/Controls/Settings/TestUtils.tsx @@ -68,7 +68,6 @@ export const collection = { dataMaskingPolicy: ko.observable({ includedPaths: [], excludedPaths: ["/excludedPath"], - isPolicyEnabled: true, }), readSettings: () => { return; diff --git a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap index 569bfd035..7f8452ddf 100644 --- a/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap +++ b/src/Explorer/Controls/Settings/__snapshots__/SettingsComponent.test.tsx.snap @@ -604,6 +604,58 @@ exports[`SettingsComponent renders 1`] = ` /> + + + + + (), excludedPaths: Array(), - isPolicyEnabled: true, }; const observablePolicy = ko.observable(data.dataMaskingPolicy || defaultDataMaskingPolicy); observablePolicy.subscribe(() => {}); diff --git a/test/sql/scaleAndSettings/dataMasking.spec.ts b/test/sql/scaleAndSettings/dataMasking.spec.ts new file mode 100644 index 000000000..0c076554f --- /dev/null +++ b/test/sql/scaleAndSettings/dataMasking.spec.ts @@ -0,0 +1,127 @@ +import { expect, test, type Page } from "@playwright/test"; +import { DataExplorer, TestAccount } from "../../fx"; +import { createTestSQLContainer, TestContainerContext } from "../../testData"; + +/** + * Tests for Dynamic Data Masking (DDM) feature. + * + * Prerequisites: + * - Test account must have the EnableDynamicDataMasking capability enabled + * - If the capability is not enabled, the DataMaskingTab will not be visible and tests will be skipped + * + * Important Notes: + * - Tests focus on enabling DDM and modifying the masking policy configuration + */ + +let testContainer: TestContainerContext; +let DATABASE_ID: string; +let CONTAINER_ID: string; + +test.beforeAll(async () => { + testContainer = await createTestSQLContainer(); + DATABASE_ID = testContainer.database.id; + CONTAINER_ID = testContainer.container.id; +}); + +// Clean up test database after all tests +test.afterAll(async () => { + if (testContainer) { + await testContainer.dispose(); + } +}); + +// Helper function to navigate to Data Masking tab +async function navigateToDataMaskingTab(page: Page, explorer: DataExplorer): Promise { + // Refresh the tree to see the newly created database + const refreshButton = explorer.frame.getByTestId("Sidebar/RefreshButton"); + await refreshButton.click(); + await page.waitForTimeout(3000); + + // Expand database and container nodes + const databaseNode = await explorer.waitForNode(DATABASE_ID); + await databaseNode.expand(); + await page.waitForTimeout(2000); + + const containerNode = await explorer.waitForNode(`${DATABASE_ID}/${CONTAINER_ID}`); + await containerNode.expand(); + await page.waitForTimeout(1000); + + // Click Scale & Settings or Settings (depending on container type) + let settingsNode = explorer.frame.getByTestId(`TreeNode:${DATABASE_ID}/${CONTAINER_ID}/Scale & Settings`); + const isScaleAndSettings = await settingsNode.isVisible().catch(() => false); + + if (!isScaleAndSettings) { + settingsNode = explorer.frame.getByTestId(`TreeNode:${DATABASE_ID}/${CONTAINER_ID}/Settings`); + } + + await settingsNode.click(); + await page.waitForTimeout(2000); + + // Check if Data Masking tab is available + const dataMaskingTab = explorer.frame.getByTestId("settings-tab-header/DataMaskingTab"); + const isTabVisible = await dataMaskingTab.isVisible().catch(() => false); + + if (!isTabVisible) { + return false; + } + + await dataMaskingTab.click(); + await page.waitForTimeout(1000); + return true; +} + +test.describe("Data Masking under Scale & Settings", () => { + test("Data Masking tab should be visible and show JSON editor", async ({ page }) => { + const explorer = await DataExplorer.open(page, TestAccount.SQL); + const isTabAvailable = await navigateToDataMaskingTab(page, explorer); + + if (!isTabAvailable) { + test.skip( + true, + "Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.", + ); + } + + // Verify the Data Masking editor is visible + const dataMaskingEditor = explorer.frame.locator(".settingsV2Editor"); + await expect(dataMaskingEditor).toBeVisible(); + }); + + test("Data Masking editor should contain default policy structure", async ({ page }) => { + const explorer = await DataExplorer.open(page, TestAccount.SQL); + const isTabAvailable = await navigateToDataMaskingTab(page, explorer); + + if (!isTabAvailable) { + test.skip( + true, + "Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.", + ); + } + + // Verify the editor contains the expected JSON structure fields + const editorContent = explorer.frame.locator(".settingsV2Editor"); + await expect(editorContent).toBeVisible(); + + // Check that the editor contains key policy fields (default policy has empty arrays) + await expect(editorContent).toContainText("includedPaths"); + await expect(editorContent).toContainText("excludedPaths"); + }); + + test("Data Masking editor should have correct default policy values", async ({ page }) => { + const explorer = await DataExplorer.open(page, TestAccount.SQL); + const isTabAvailable = await navigateToDataMaskingTab(page, explorer); + + if (!isTabAvailable) { + test.skip( + true, + "Data Masking tab is not available. Test account may not have EnableDynamicDataMasking capability.", + ); + } + + const editorContent = explorer.frame.locator(".settingsV2Editor"); + await expect(editorContent).toBeVisible(); + + // Default policy should have empty includedPaths and excludedPaths arrays + await expect(editorContent).toContainText("[]"); + }); +});