diff --git a/package-lock.json b/package-lock.json index e9cef16a7..c241beafc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -875,9 +875,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -1752,15 +1753,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.0.tgz", - "integrity": "sha512-YPJfjQPDXxyQWg/0+jHKj1llnY5f/R6a0p/vP4lPymxLu7Lvl4k2WMitqi08yxwQcCVUUdG9LCUj4TNEgAp3Jw==", + "version": "7.29.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.4.tgz", + "integrity": "sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.0", - "@babel/helper-plugin-utils": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.0" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -10991,13 +10993,13 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -15560,9 +15562,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -15570,6 +15572,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, diff --git a/src/Common/dataAccess/createCollection.ts b/src/Common/dataAccess/createCollection.ts index 7f531211c..91031034b 100644 --- a/src/Common/dataAccess/createCollection.ts +++ b/src/Common/dataAccess/createCollection.ts @@ -34,6 +34,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams databaseId: params.databaseId, databaseLevelThroughput: params.databaseLevelThroughput, offerThroughput: params.offerThroughput, + targetAccountOverride: params.targetAccountOverride, }; await createDatabase(createDatabaseParams); } @@ -63,7 +64,7 @@ export const createCollection = async (params: DataModels.CreateCollectionParams }; const createCollectionWithARM = async (params: DataModels.CreateCollectionParams): Promise => { - if (!params.createNewDatabase) { + if (!params.createNewDatabase && !params.targetAccountOverride) { const isValid = await useDatabases.getState().validateCollectionId(params.databaseId, params.collectionId); if (!isValid) { const collectionName = getCollectionName().toLocaleLowerCase(); @@ -122,9 +123,9 @@ const createSqlContainer = async (params: DataModels.CreateCollectionParams): Pr }; const createResponse = await createUpdateSqlContainer( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, + params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId, + params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup, + params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name, params.databaseId, params.collectionId, rpPayload, diff --git a/src/Common/dataAccess/createDatabase.test.ts b/src/Common/dataAccess/createDatabase.test.ts new file mode 100644 index 000000000..3fca00378 --- /dev/null +++ b/src/Common/dataAccess/createDatabase.test.ts @@ -0,0 +1,137 @@ +jest.mock("../../Utils/arm/request"); +jest.mock("../CosmosClient"); +jest.mock("../../Utils/arm/generatedClients/cosmos/sqlResources"); + +import ko from "knockout"; +import { AuthType } from "../../AuthType"; +import { CreateDatabaseParams, DatabaseAccount } from "../../Contracts/DataModels"; +import * as ViewModels from "../../Contracts/ViewModels"; +import { useDatabases } from "../../Explorer/useDatabases"; +import { updateUserContext } from "../../UserContext"; +import { createUpdateSqlDatabase } from "../../Utils/arm/generatedClients/cosmos/sqlResources"; +import { SqlDatabaseGetResults } from "../../Utils/arm/generatedClients/cosmos/types"; +import { createDatabase } from "./createDatabase"; + +const mockCreateUpdateSqlDatabase = createUpdateSqlDatabase as jest.MockedFunction; + +describe("createDatabase", () => { + beforeAll(() => { + updateUserContext({ + databaseAccount: { name: "default-account" } as DatabaseAccount, + subscriptionId: "default-subscription", + resourceGroup: "default-rg", + apiType: "SQL", + authType: AuthType.AAD, + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockCreateUpdateSqlDatabase.mockResolvedValue({ + properties: { resource: { id: "db", _rid: "", _self: "", _ts: 0, _etag: "" } }, + } as SqlDatabaseGetResults); + useDatabases.setState({ + databases: [], + validateDatabaseId: () => true, + } as unknown as ReturnType); + }); + + it("should call ARM createUpdateSqlDatabase when logged in with AAD", async () => { + await createDatabase({ databaseId: "testDb" }); + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled(); + }); + + describe("targetAccountOverride behavior", () => { + it("should use targetAccountOverride subscriptionId, resourceGroup, and accountName for SQL DB creation", async () => { + const params: CreateDatabaseParams = { + databaseId: "testDb", + targetAccountOverride: { + subscriptionId: "override-sub", + resourceGroup: "override-rg", + accountName: "override-account", + capabilities: [], + }, + }; + + await createDatabase(params); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + "override-sub", + "override-rg", + "override-account", + "testDb", + expect.any(Object), + ); + }); + + it("should use userContext values when targetAccountOverride is not provided", async () => { + await createDatabase({ databaseId: "testDb" }); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + "default-subscription", + "default-rg", + "default-account", + "testDb", + expect.any(Object), + ); + }); + + it("should skip validateDatabaseId check when targetAccountOverride is provided", async () => { + // Simulate database already existing — validateDatabaseId returns false + useDatabases.setState({ + databases: [{ id: ko.observable("testDb") } as unknown as ViewModels.Database], + validateDatabaseId: () => false, + } as unknown as ReturnType); + + const params: CreateDatabaseParams = { + databaseId: "testDb", + targetAccountOverride: { + subscriptionId: "override-sub", + resourceGroup: "override-rg", + accountName: "override-account", + capabilities: [], + }, + }; + + // Should NOT throw even though the normal duplicate check would fail + await expect(createDatabase(params)).resolves.not.toThrow(); + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalled(); + }); + + it("should throw if validateDatabaseId returns false and no targetAccountOverride is set", async () => { + useDatabases.setState({ + databases: [{ id: ko.observable("existingDb") } as unknown as ViewModels.Database], + validateDatabaseId: () => false, + } as unknown as ReturnType); + + await expect(createDatabase({ databaseId: "existingDb" })).rejects.toThrow(); + expect(mockCreateUpdateSqlDatabase).not.toHaveBeenCalled(); + }); + + it("should pass databaseId in request payload regardless of targetAccountOverride", async () => { + const params: CreateDatabaseParams = { + databaseId: "my-database", + targetAccountOverride: { + subscriptionId: "any-sub", + resourceGroup: "any-rg", + accountName: "any-account", + capabilities: [], + }, + }; + + await createDatabase(params); + + expect(mockCreateUpdateSqlDatabase).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + expect.any(String), + "my-database", + expect.objectContaining({ + properties: expect.objectContaining({ + resource: expect.objectContaining({ id: "my-database" }), + }), + }), + ); + }); + }); +}); diff --git a/src/Common/dataAccess/createDatabase.ts b/src/Common/dataAccess/createDatabase.ts index c562c4068..31190cfe7 100644 --- a/src/Common/dataAccess/createDatabase.ts +++ b/src/Common/dataAccess/createDatabase.ts @@ -41,7 +41,7 @@ export async function createDatabase(params: DataModels.CreateDatabaseParams): P } async function createDatabaseWithARM(params: DataModels.CreateDatabaseParams): Promise { - if (!useDatabases.getState().validateDatabaseId(params.databaseId)) { + if (!params.targetAccountOverride && !useDatabases.getState().validateDatabaseId(params.databaseId)) { const databaseName = getDatabaseName().toLocaleLowerCase(); throw new Error(`Create ${databaseName} failed: ${databaseName} with id ${params.databaseId} already exists`); } @@ -72,13 +72,10 @@ async function createSqlDatabase(params: DataModels.CreateDatabaseParams): Promi options, }, }; - const createResponse = await createUpdateSqlDatabase( - userContext.subscriptionId, - userContext.resourceGroup, - userContext.databaseAccount.name, - params.databaseId, - rpPayload, - ); + const sub = params.targetAccountOverride?.subscriptionId ?? userContext.subscriptionId; + const rg = params.targetAccountOverride?.resourceGroup ?? userContext.resourceGroup; + const acct = params.targetAccountOverride?.accountName ?? userContext.databaseAccount.name; + const createResponse = await createUpdateSqlDatabase(sub, rg, acct, params.databaseId, rpPayload); return createResponse && (createResponse.properties.resource as DataModels.Database); } diff --git a/src/Common/dataAccess/readDatabases.test.ts b/src/Common/dataAccess/readDatabases.test.ts index 8f6368f78..0d0eb90fc 100644 --- a/src/Common/dataAccess/readDatabases.test.ts +++ b/src/Common/dataAccess/readDatabases.test.ts @@ -1,11 +1,12 @@ jest.mock("../../Utils/arm/request"); jest.mock("../CosmosClient"); + import { AuthType } from "../../AuthType"; import { DatabaseAccount } from "../../Contracts/DataModels"; import { updateUserContext } from "../../UserContext"; import { armRequest } from "../../Utils/arm/request"; import { client } from "../CosmosClient"; -import { readDatabases } from "./readDatabases"; +import { readDatabases, readDatabasesWithARM } from "./readDatabases"; describe("readDatabases", () => { beforeAll(() => { @@ -42,3 +43,149 @@ describe("readDatabases", () => { expect(client).toHaveBeenCalled(); }); }); + +describe("readDatabasesWithARM (with accountOverride)", () => { + const mockDatabase = { id: "testDb", _rid: "", _self: "", _etag: "", _ts: 0 }; + const mockArmResponse = { value: [{ properties: { resource: mockDatabase } }] }; + + beforeAll(() => { + updateUserContext({ + databaseAccount: { name: "context-account" } as DatabaseAccount, + subscriptionId: "context-sub", + resourceGroup: "context-rg", + apiType: "SQL", + }); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should call ARM with a path that includes the provided subscriptionId, resourceGroup, and accountName", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "test-sub", resourceGroup: "test-rg", accountName: "test-account" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining("/subscriptions/test-sub/resourceGroups/test-rg/"), + }), + ); + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringContaining("/databaseAccounts/test-account/sqlDatabases"), + }), + ); + }); + + it("should use apiType from accountOverride when provided (SQL)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "SQL" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/sqlDatabases") }), + ); + }); + + it("should use apiType from accountOverride when provided (Mongo)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Mongo", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }), + ); + }); + + it("should use apiType from accountOverride when provided (Cassandra)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Cassandra", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/cassandraKeyspaces") }), + ); + }); + + it("should use apiType from accountOverride when provided (Gremlin)", async () => { + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ + subscriptionId: "sub", + resourceGroup: "rg", + accountName: "account", + apiType: "Gremlin", + }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/gremlinDatabases") }), + ); + }); + + it("should fall back to userContext.apiType when apiType is not in accountOverride", async () => { + updateUserContext({ apiType: "Mongo" }); + (armRequest as jest.Mock).mockResolvedValue(mockArmResponse); + + await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(armRequest).toHaveBeenCalledWith( + expect.objectContaining({ path: expect.stringContaining("/mongodbDatabases") }), + ); + + updateUserContext({ apiType: "SQL" }); // restore + }); + + it("should throw for unsupported apiType", async () => { + await expect( + readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account", apiType: "Tables" }), + ).rejects.toThrow("Unsupported default experience type: Tables"); + }); + + it("should return mapped database resources from the response", async () => { + const db1 = { id: "db1", _rid: "r1", _self: "/dbs/db1", _etag: "", _ts: 1 }; + const db2 = { id: "db2", _rid: "r2", _self: "/dbs/db2", _etag: "", _ts: 2 }; + + (armRequest as jest.Mock).mockResolvedValue({ + value: [{ properties: { resource: db1 } }, { properties: { resource: db2 } }], + }); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([db1, db2]); + }); + + it("should return an empty array when the response is null", async () => { + (armRequest as jest.Mock).mockResolvedValue(null); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([]); + }); + + it("should return an empty array when value is an empty list", async () => { + (armRequest as jest.Mock).mockResolvedValue({ value: [] }); + + const result = await readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }); + + expect(result).toEqual([]); + }); + + it("should throw and propagate errors from the ARM call", async () => { + (armRequest as jest.Mock).mockRejectedValue(new Error("ARM request failed")); + + await expect( + readDatabasesWithARM({ subscriptionId: "sub", resourceGroup: "rg", accountName: "account" }), + ).rejects.toThrow("ARM request failed"); + }); +}); diff --git a/src/Common/dataAccess/readDatabases.ts b/src/Common/dataAccess/readDatabases.ts index 85b1c3186..e9f43e65f 100644 --- a/src/Common/dataAccess/readDatabases.ts +++ b/src/Common/dataAccess/readDatabases.ts @@ -4,7 +4,7 @@ import { AuthType } from "../../AuthType"; import * as DataModels from "../../Contracts/DataModels"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; -import { FabricArtifactInfo, userContext } from "../../UserContext"; +import { ApiType, FabricArtifactInfo, userContext } from "../../UserContext"; import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources"; import { listGremlinDatabases } from "../../Utils/arm/generatedClients/cosmos/gremlinResources"; @@ -96,10 +96,17 @@ export async function readDatabases(): Promise { return databases; } -async function readDatabasesWithARM(): Promise { +export async function readDatabasesWithARM(accountOverride?: { + subscriptionId: string; + resourceGroup: string; + accountName: string; + apiType?: ApiType; +}): Promise { let rpResponse; - const { subscriptionId, resourceGroup, apiType, databaseAccount } = userContext; - const accountName = databaseAccount.name; + const subscriptionId = accountOverride?.subscriptionId ?? userContext.subscriptionId ?? ""; + const resourceGroup = accountOverride?.resourceGroup ?? userContext.resourceGroup ?? ""; + const accountName = accountOverride?.accountName ?? userContext?.databaseAccount?.name ?? ""; + const apiType = accountOverride?.apiType ?? userContext.apiType; switch (apiType) { case "SQL": @@ -118,5 +125,5 @@ async function readDatabasesWithARM(): Promise { throw new Error(`Unsupported default experience type: ${apiType}`); } - return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database); + return rpResponse?.value?.map((database) => database.properties?.resource as DataModels.Database) ?? []; } diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index a72ac434e..770ee7948 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -406,11 +406,22 @@ export interface AutoPilotOfferSettings { targetMaxThroughput?: number; } +export interface AccountOverride { + subscriptionId: string; + resourceGroup: string; + accountName: string; + capabilities: Capability[]; + capacityMode?: CapacityMode; + enableFreeTier?: boolean; + enableAnalyticalStorage?: boolean; +} + export interface CreateDatabaseParams { autoPilotMaxThroughput?: number; databaseId: string; databaseLevelThroughput?: boolean; offerThroughput?: number; + targetAccountOverride?: AccountOverride; } export interface CreateCollectionParamsBase { @@ -430,6 +441,7 @@ export interface CreateCollectionParamsBase { export interface CreateCollectionParams extends CreateCollectionParamsBase { createNewDatabase: boolean; collectionId: string; + targetAccountOverride?: AccountOverride; } export interface CreateMaterializedViewsParams extends CreateCollectionParamsBase { diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx index 9bb7fe3ea..b491f6bf5 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.test.tsx @@ -457,13 +457,13 @@ describe("CopyJobActions", () => { jobName: "test-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -498,7 +498,7 @@ describe("CopyJobActions", () => { ); const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; - expect(callArgs.properties.source.remoteAccountName).toBeUndefined(); + expect(callArgs.properties.destination.remoteAccountName).toBeUndefined(); expect(mockRefreshJobList).toHaveBeenCalled(); expect(mockOnSuccess).toHaveBeenCalled(); @@ -509,13 +509,13 @@ describe("CopyJobActions", () => { jobName: "cross-account-job", migrationType: "offline" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-456", + subscription: {} as any, account: { id: "account-2", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -528,7 +528,7 @@ describe("CopyJobActions", () => { await submitCreateCopyJob(mockState, mockOnSuccess); const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4]; - expect(callArgs.properties.source.remoteAccountName).toBe("source-account"); + expect(callArgs.properties.destination.remoteAccountName).toBe("target-account"); expect(mockOnSuccess).toHaveBeenCalled(); }); @@ -537,13 +537,13 @@ describe("CopyJobActions", () => { jobName: "failing-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", @@ -566,13 +566,13 @@ describe("CopyJobActions", () => { jobName: "test-job", migrationType: "online" as any, source: { - subscription: {} as any, + subscriptionId: "sub-123", account: { id: "account-1", name: "source-account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "sub-123", + subscription: {} as any, account: { id: "account-1", name: "target-account" } as any, databaseId: "target-db", containerId: "target-container", diff --git a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx index ec8f60be7..359243d10 100644 --- a/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx +++ b/src/Explorer/ContainerCopy/Actions/CopyJobActions.tsx @@ -1,4 +1,5 @@ import Explorer from "Explorer/Explorer"; +import { Keys, t } from "Localization"; import React from "react"; import { userContext } from "UserContext"; import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers"; @@ -15,7 +16,6 @@ import { CreateJobRequest, DataTransferJobGetResults, } from "../../../Utils/arm/generatedClients/dataTransferService/types"; -import { Keys, t } from "Localization"; import { convertTime, convertToCamelCase, @@ -137,12 +137,12 @@ export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: properties: { source: { component: "CosmosDBSql", - ...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }), databaseName: source?.databaseId, containerName: source?.containerId, }, destination: { component: "CosmosDBSql", + ...(isSameAccount ? {} : { remoteAccountName: target?.account?.name }), databaseName: target?.databaseId, containerName: target?.containerId, }, diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx index 7a0e7d874..f263f3cb3 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.test.tsx @@ -59,12 +59,6 @@ describe("CopyJobContext", () => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { subscriptionId: "test-subscription-id", account: { id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account", @@ -75,7 +69,13 @@ describe("CopyJobContext", () => { databaseId: "", containerId: "", }, - sourceReadAccessFromTarget: false, + target: { + subscription: null, + account: null, + databaseId: "", + containerId: "", + }, + sourceReadWriteAccessFromTarget: false, }); expect(contextValue.flow).toBeNull(); expect(contextValue.contextError).toBeNull(); @@ -598,8 +598,8 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined(); - expect(contextValue.copyJobState.source?.account?.name).toBeUndefined(); + expect(contextValue.copyJobState.source?.subscriptionId).toBe("test-subscription-id"); + expect(contextValue.copyJobState.source?.account?.name).toBe("test-account"); }); it("should initialize target with userContext values", () => { @@ -616,11 +616,11 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id"); - expect(contextValue.copyJobState.target.account.name).toBe("test-account"); + expect(contextValue.copyJobState.target.subscription).toBeNull(); + expect(contextValue.copyJobState.target.account).toBeNull(); }); - it("should initialize sourceReadAccessFromTarget as false", () => { + it("should initialize sourceReadWriteAccessFromTarget as false", () => { let contextValue: any; render( @@ -634,7 +634,7 @@ describe("CopyJobContext", () => { , ); - expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false); + expect(contextValue.copyJobState.sourceReadWriteAccessFromTarget).toBe(false); }); it("should initialize with empty database and container ids", () => { diff --git a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx index ddb936dcf..912ed0311 100644 --- a/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx +++ b/src/Explorer/ContainerCopy/Context/CopyJobContext.tsx @@ -23,18 +23,18 @@ const getInitialCopyJobState = (): CopyJobContextState => { jobName: "", migrationType: CopyJobMigrationType.Offline, source: { - subscription: null, - account: null, - databaseId: "", - containerId: "", - }, - target: { subscriptionId: userContext.subscriptionId || "", account: userContext.databaseAccount || null, databaseId: "", containerId: "", }, - sourceReadAccessFromTarget: false, + target: { + subscription: null, + account: null, + databaseId: "", + containerId: "", + }, + sourceReadWriteAccessFromTarget: false, }; }; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx index 98ef7fae3..0e35c245c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.test.tsx @@ -67,7 +67,7 @@ describe("AddManagedIdentity", () => { databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, }; const mockContextValue = { @@ -197,11 +197,10 @@ describe("AddManagedIdentity", () => { }); it("displays correct enablement description with account name", () => { - expect( - screen.getByText( - t(Keys.containerCopy.addManagedIdentity.enablementDescription, { accountName: "test-target-account" }), - ), - ).toBeInTheDocument(); + const expectedDescription = t(Keys.containerCopy.addManagedIdentity.enablementDescription, { + accountName: mockCopyJobState.source.account.name, + }); + expect(screen.getByText(expectedDescription)).toBeInTheDocument(); }); it("calls handleAddSystemIdentity when primary button clicked", async () => { diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx index c2387580a..fbc7baec0 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddManagedIdentity.tsx @@ -39,7 +39,13 @@ const AddManagedIdentity: React.FC = () => {   - + = () => { onCancel={() => onToggle(null, false)} onPrimary={handleAddSystemIdentity} > - {copyJobState.target?.account?.name - ? t(Keys.containerCopy.addManagedIdentity.enablementDescription, { - accountName: copyJobState.target?.account?.name, - }) - : ""} + {t(Keys.containerCopy.addManagedIdentity.enablementDescription, { + accountName: copyJobState.source?.account?.name, + })} ); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx index 69c5bcc93..c86183ac1 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.test.tsx @@ -4,7 +4,7 @@ import { Keys, t } from "Localization"; import React from "react"; import { CopyJobContext } from "../../../Context/CopyJobContext"; import { CopyJobContextProviderType } from "../../../Types/CopyJobTypes"; -import AddReadPermissionToDefaultIdentity from "./AddReadPermissionToDefaultIdentity"; +import AddReadWritePermissionToDefaultIdentity from "./AddReadWritePermissionToDefaultIdentity"; jest.mock("../../../../../Common/Logger", () => ({ logError: jest.fn(), @@ -73,7 +73,7 @@ import { assignRole, RoleAssignmentType } from "../../../../../Utils/arm/RbacUti import { getAccountDetailsFromResourceId } from "../../../CopyJobUtils"; import useToggle from "./hooks/useToggle"; -describe("AddReadPermissionToDefaultIdentity Component", () => { +describe("AddReadWritePermissionToDefaultIdentity Component", () => { const mockUseToggle = useToggle as jest.MockedFunction; const mockAssignRole = assignRole as jest.MockedFunction; const mockGetAccountDetailsFromResourceId = getAccountDetailsFromResourceId as jest.MockedFunction< @@ -86,7 +86,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "source-sub-id" } as Subscription, + subscriptionId: "source-sub-id", account: { id: "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", name: "source-account", @@ -96,12 +96,16 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { properties: { documentEndpoint: "https://source-account.documents.azure.com:443/", }, + identity: { + principalId: "source-principal-id", + type: "SystemAssigned", + }, }, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "target-sub-id", + subscription: { subscriptionId: "target-sub-id" } as Subscription, account: { id: "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account", name: "target-account", @@ -119,7 +123,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, }, setCopyJobState: jest.fn(), setContextError: jest.fn(), @@ -133,7 +137,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { const renderComponent = (contextValue = mockContextValue) => { return render( - + , ); }; @@ -164,12 +168,12 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(container).toMatchSnapshot(); }); - it("should render correctly when sourceReadAccessFromTarget is true", () => { + it("should render correctly when sourceReadWriteAccessFromTarget is true", () => { const contextWithAccess = { ...mockContextValue, copyJobState: { ...mockContextValue.copyJobState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, }, }; const { container } = renderComponent(contextWithAccess); @@ -180,7 +184,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { describe("Component Structure", () => { it("should display the description text", () => { renderComponent(); - expect(screen.getByText(t(Keys.containerCopy.readPermissionAssigned.description))).toBeInTheDocument(); + expect(screen.getByText(t(Keys.containerCopy.readWritePermissionAssigned.description))).toBeInTheDocument(); }); it("should display the info tooltip", () => { @@ -212,10 +216,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(screen.getByTestId("popover-message")).toBeInTheDocument(); expect(screen.getByTestId("popover-title")).toHaveTextContent( - t(Keys.containerCopy.readPermissionAssigned.popoverTitle), + t(Keys.containerCopy.readWritePermissionAssigned.popoverTitle), ); expect(screen.getByTestId("popover-content")).toHaveTextContent( - t(Keys.containerCopy.readPermissionAssigned.popoverDescription), + t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription), ); }); @@ -243,11 +247,11 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(mockOnToggle).toHaveBeenCalledWith(null, false); }); - it("should call handleAddReadPermission when primary button is clicked", async () => { + it("should call handleAddReadWritePermission when primary button is clicked", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -258,22 +262,22 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockGetAccountDetailsFromResourceId).toHaveBeenCalledWith( - "/subscriptions/source-sub-id/resourceGroups/source-rg/providers/Microsoft.DocumentDB/databaseAccounts/source-account", + "/subscriptions/target-sub-id/resourceGroups/target-rg/providers/Microsoft.DocumentDB/databaseAccounts/target-account", ); }); }); }); - describe("handleAddReadPermission Function", () => { + describe("handleAddReadWritePermission Function", () => { beforeEach(() => { mockUseToggle.mockReturnValue([true, jest.fn()]); }); it("should successfully assign role and update context", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -284,10 +288,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockAssignRole).toHaveBeenCalledWith( - "source-sub-id", - "source-rg", - "source-account", - "target-principal-id", + "target-sub-id", + "target-rg", + "target-account", + "source-principal-id", ); }); @@ -298,9 +302,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it("should handle error when assignRole fails", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockRejectedValue(new Error("Permission denied")); @@ -312,7 +316,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockLogError).toHaveBeenCalledWith( "Permission denied", - "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", + "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission", ); }); @@ -323,9 +327,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it("should handle error without message", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockRejectedValue({}); @@ -336,23 +340,23 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { await waitFor(() => { expect(mockLogError).toHaveBeenCalledWith( - "Error assigning read permission to default identity. Please try again later.", - "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission", + "Error assigning read-write permission to default identity. Please try again later.", + "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission", ); }); await waitFor(() => { expect(mockContextValue.setContextError).toHaveBeenCalledWith( - "Error assigning read permission to default identity. Please try again later.", + "Error assigning read-write permission to default identity. Please try again later.", ); }); }); it("should show loading state during role assignment", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockImplementation( @@ -371,9 +375,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { it.skip("should not assign role when assignRole returns falsy", async () => { mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue(null); @@ -431,10 +435,10 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { ...mockContextValue, copyJobState: { ...mockContextValue.copyJobState, - target: { - ...mockContextValue.copyJobState.target, + source: { + ...mockContextValue.copyJobState.source, account: { - ...mockContextValue.copyJobState.target.account!, + ...mockContextValue.copyJobState.source.account!, identity: { principalId: "", type: "SystemAssigned", @@ -446,9 +450,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { mockUseToggle.mockReturnValue([true, jest.fn()]); mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -458,7 +462,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { fireEvent.click(primaryButton); await waitFor(() => { - expect(mockAssignRole).toHaveBeenCalledWith("source-sub-id", "source-rg", "source-account", ""); + expect(mockAssignRole).toHaveBeenCalledWith("target-sub-id", "target-rg", "target-account", ""); }); }); }); @@ -476,9 +480,9 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { mockUseToggle.mockReturnValue([true, jest.fn()]); mockGetAccountDetailsFromResourceId.mockReturnValue({ - subscriptionId: "source-sub-id", - resourceGroup: "source-rg", - accountName: "source-account", + subscriptionId: "target-sub-id", + resourceGroup: "target-rg", + accountName: "target-account", }); mockAssignRole.mockResolvedValue({ id: "role-assignment-id" } as RoleAssignmentType); @@ -496,7 +500,7 @@ describe("AddReadPermissionToDefaultIdentity Component", () => { expect(updatedState).toEqual({ ...mockContextValue.copyJobState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, }); }); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx similarity index 52% rename from src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx rename to src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx index 691c8f1bc..d042d15a7 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadPermissionToDefaultIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AddReadWritePermissionToDefaultIdentity.tsx @@ -12,51 +12,54 @@ import useToggle from "./hooks/useToggle"; const TooltipContent = ( - {t(Keys.containerCopy.readPermissionAssigned.tooltipContent)}   + {t(Keys.containerCopy.readWritePermissionAssigned.tooltipContent)}   - {t(Keys.containerCopy.readPermissionAssigned.tooltipHrefText)} + {t(Keys.containerCopy.readWritePermissionAssigned.tooltipHrefText)} ); -type AddReadPermissionToDefaultIdentityProps = Partial; -const AddReadPermissionToDefaultIdentity: React.FC = () => { +type AddReadWritePermissionToDefaultIdentityProps = Partial; + +const AddReadWritePermissionToDefaultIdentity: React.FC = () => { const [loading, setLoading] = React.useState(false); const { copyJobState, setCopyJobState, setContextError } = useCopyJobContext(); - const [readPermissionAssigned, onToggle] = useToggle(false); + const [readWritePermissionAssigned, onToggle] = useToggle(copyJobState.sourceReadWriteAccessFromTarget ?? false); - const handleAddReadPermission = async () => { + const handleAddReadWritePermission = async () => { const { source, target } = copyJobState; - const selectedSourceAccount = source?.account; + const selectedTargetAccount = target?.account; + try { const { - subscriptionId: sourceSubscriptionId, - resourceGroup: sourceResourceGroup, - accountName: sourceAccountName, - } = getAccountDetailsFromResourceId(selectedSourceAccount?.id); + subscriptionId: targetSubscriptionId, + resourceGroup: targetResourceGroup, + accountName: targetAccountName, + } = getAccountDetailsFromResourceId(selectedTargetAccount?.id); setLoading(true); const assignedRole = await assignRole( - sourceSubscriptionId, - sourceResourceGroup, - sourceAccountName, - target?.account?.identity?.principalId ?? "", + targetSubscriptionId, + targetResourceGroup, + targetAccountName, + source?.account?.identity?.principalId ?? "", ); + if (assignedRole) { setCopyJobState((prevState) => ({ ...prevState, - sourceReadAccessFromTarget: true, + sourceReadWriteAccessFromTarget: true, })); } } catch (error) { const errorMessage = - error.message || "Error assigning read permission to default identity. Please try again later."; - logError(errorMessage, "CopyJob/AddReadPermissionToDefaultIdentity.handleAddReadPermission"); + error.message || "Error assigning read-write permission to default identity. Please try again later."; + logError(errorMessage, "CopyJob/AddReadWritePermissionToDefaultIdentity.handleAddReadWritePermission"); setContextError(errorMessage); } finally { setLoading(false); @@ -66,14 +69,14 @@ const AddReadPermissionToDefaultIdentity: React.FC - {t(Keys.containerCopy.readPermissionAssigned.description)}  + {t(Keys.containerCopy.readWritePermissionAssigned.description)}  onToggle(null, false)} - onPrimary={handleAddReadPermission} + onPrimary={handleAddReadWritePermission} > - {t(Keys.containerCopy.readPermissionAssigned.popoverDescription)} + {t(Keys.containerCopy.readWritePermissionAssigned.popoverDescription)} ); }; -export default AddReadPermissionToDefaultIdentity; +export default AddReadWritePermissionToDefaultIdentity; diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx index 1147f6b67..f0cd65cf3 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.test.tsx @@ -43,12 +43,12 @@ jest.mock("./AddManagedIdentity", () => { return MockAddManagedIdentity; }); -jest.mock("./AddReadPermissionToDefaultIdentity", () => { - const MockAddReadPermissionToDefaultIdentity = () => { - return
Add Read Permission Component
; +jest.mock("./AddReadWritePermissionToDefaultIdentity", () => { + const MockAddReadWritePermissionToDefaultIdentity = () => { + return
Add Read-Write Permission Component
; }; - MockAddReadPermissionToDefaultIdentity.displayName = "MockAddReadPermissionToDefaultIdentity"; - return MockAddReadPermissionToDefaultIdentity; + MockAddReadWritePermissionToDefaultIdentity.displayName = "MockAddReadWritePermissionToDefaultIdentity"; + return MockAddReadWritePermissionToDefaultIdentity; }); jest.mock("./DefaultManagedIdentity", () => { @@ -85,18 +85,18 @@ describe("AssignPermissions Component", () => { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "source-sub" } as any, + subscriptionId: "source-sub", account: { id: "source-account", name: "Source Account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "target-sub", + subscription: { subscriptionId: "target-sub" } as any, account: { id: "target-account", name: "Target Account" } as any, databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, ...overrides, }); @@ -164,13 +164,13 @@ describe("AssignPermissions Component", () => { const copyJobState = createMockCopyJobState({ migrationType: CopyJobMigrationType.Online, source: { - subscription: { subscriptionId: "same-sub" } as any, + subscriptionId: "same-sub", account: { id: "same-account", name: "Same Account" } as any, databaseId: "source-db", containerId: "source-container", }, target: { - subscriptionId: "same-sub", + subscription: { subscriptionId: "same-sub" } as any, account: { id: "same-account", name: "Same Account" } as any, databaseId: "target-db", containerId: "target-container", @@ -203,7 +203,7 @@ describe("AssignPermissions Component", () => { completed: true, }, { - id: "readPermissionAssigned", + id: "readWritePermissionAssigned", title: "Read Permission Assigned", Component: () =>
Add Read Permission Component
, disabled: false, @@ -349,7 +349,7 @@ describe("AssignPermissions Component", () => { it("should handle missing account names", () => { const copyJobState = createMockCopyJobState({ source: { - subscription: { subscriptionId: "source-sub" } as any, + subscriptionId: "source-sub", account: { id: "source-account" } as any, databaseId: "source-db", containerId: "source-container", diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx index b8c3b9bc7..43096e121 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/AssignPermissions.tsx @@ -1,10 +1,10 @@ import { Image, Stack, Text } from "@fluentui/react"; import { Accordion, AccordionHeader, AccordionItem, AccordionPanel } from "@fluentui/react-components"; +import { Keys, t } from "Localization"; import React, { useEffect } from "react"; import CheckmarkIcon from "../../../../../../images/successfulPopup.svg"; import WarningIcon from "../../../../../../images/warning.svg"; import ShimmerTree, { IndentLevel } from "../../../../../Common/ShimmerTree/ShimmerTree"; -import { Keys, t } from "Localization"; import { useCopyJobContext } from "../../../Context/CopyJobContext"; import { isIntraAccountCopy } from "../../../CopyJobUtils"; import { CopyJobMigrationType } from "../../../Enums/CopyJobEnums"; @@ -106,7 +106,7 @@ const AssignPermissions = () => { tokens={{ childrenGap: 20 }} > - {isSameAccount && copyJobState.migrationType === CopyJobMigrationType.Online + {isSameAccount && copyJobState?.migrationType === CopyJobMigrationType.Online ? t(Keys.containerCopy.assignPermissions.intraAccountOnlineDescription, { accountName: copyJobState?.source?.account?.name || "", }) diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx index 08e6b55cb..201e10bfa 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.test.tsx @@ -69,6 +69,12 @@ const mockUseToggle = useToggle as jest.MockedFunction; describe("DefaultManagedIdentity", () => { const mockCopyJobContextValue = { copyJobState: { + source: { + account: { + name: "test-cosmos-account", + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account", + }, + }, target: { account: { name: "test-cosmos-account", @@ -117,7 +123,7 @@ describe("DefaultManagedIdentity", () => { renderComponent(); const description = screen.getByText( - t(Keys.containerCopy.defaultManagedIdentity.description, { accountName: "test-cosmos-account" }), + /Set the system-assigned managed identity as default for "test-cosmos-account"/, ); expect(description).toBeInTheDocument(); }); @@ -127,8 +133,8 @@ describe("DefaultManagedIdentity", () => { const tooltip = screen.getByTestId("info-tooltip"); expect(tooltip).toBeInTheDocument(); - expect(tooltip).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.tooltipContent)); - expect(tooltip).toHaveTextContent(t(Keys.containerCopy.defaultManagedIdentity.tooltipHrefText)); + expect(tooltip).toHaveTextContent("Learn more about"); + expect(tooltip).toHaveTextContent("Default Managed Identities."); }); it("should render the toggle button with correct initial state", () => { @@ -170,7 +176,7 @@ describe("DefaultManagedIdentity", () => { const content = screen.getByTestId("popover-content"); expect(content).toHaveTextContent( - t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, { accountName: "test-cosmos-account" }).trim(), + /Assign the system-assigned managed identity as the default for "test-cosmos-account"/, ); }); @@ -260,6 +266,12 @@ describe("DefaultManagedIdentity", () => { const contextValueWithoutAccount = { ...mockCopyJobContextValue, copyJobState: { + source: { + account: { + name: "", + id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/", + }, + }, target: { account: { name: "", @@ -277,6 +289,9 @@ describe("DefaultManagedIdentity", () => { const contextValueWithNullAccount = { ...mockCopyJobContextValue, copyJobState: { + source: { + account: null as DatabaseAccount | null, + }, target: { account: null as DatabaseAccount | null, }, @@ -339,8 +354,8 @@ describe("DefaultManagedIdentity", () => { it("should display correct toggle button text", () => { renderComponent(); - const onText = screen.queryByText("On"); - const offText = screen.queryByText("Off"); + const onText = screen.queryByText(t(Keys.common.on)); + const offText = screen.queryByText(t(Keys.common.off)); expect(onText || offText).toBeTruthy(); }); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx index 262e55c17..aa76220ee 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/DefaultManagedIdentity.tsx @@ -32,7 +32,9 @@ const DefaultManagedIdentity: React.FC = () => { return (
- {t(Keys.containerCopy.defaultManagedIdentity.description, { accountName: copyJobState?.target?.account?.name })}{" "} + {t(Keys.containerCopy.defaultManagedIdentity.description, { + accountName: copyJobState?.source?.account?.name, + })}{" "}  
@@ -56,7 +58,7 @@ const DefaultManagedIdentity: React.FC = () => { onPrimary={handleAddSystemIdentity} > {t(Keys.containerCopy.defaultManagedIdentity.popoverDescription, { - accountName: copyJobState?.target?.account?.name, + accountName: copyJobState?.source?.account?.name, })}
diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx index 92d68281d..5e877fcdd 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.test.tsx @@ -136,7 +136,7 @@ describe("OnlineCopyEnabled", () => { renderComponent(); const refreshButton = screen.queryByRole("button", { - name: "Refresh", + name: t(Keys.common.refresh), }); expect(refreshButton).not.toBeInTheDocument(); }); @@ -322,7 +322,7 @@ describe("OnlineCopyEnabled", () => { }); const refreshButton = screen.getByRole("button", { - name: "Refresh", + name: t(Keys.common.refresh), }); await act(async () => { @@ -454,7 +454,7 @@ describe("OnlineCopyEnabled", () => { mockFetchDatabaseAccount.mockImplementation(() => new Promise(() => {})); const refreshButton = screen.getByRole("button", { - name: "Refresh", + name: t(Keys.common.refresh), }); await act(async () => { diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx index fb3c199e5..ae6f6b728 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/OnlineCopyEnabled.tsx @@ -91,7 +91,9 @@ const OnlineCopyEnabled: React.FC = () => { }); } setLoaderMessage( - t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { accountName: sourceAccountName }), + t(Keys.containerCopy.onlineCopyEnabled.enablingOnlineCopySpinnerLabel, { + accountName: sourceAccountName, + }), ); await updateDatabaseAccount(sourceSubscriptionId, sourceResourceGroup, sourceAccountName, { properties: { diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.test.tsx b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.test.tsx index de5ddcc49..ee3e7859b 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.test.tsx +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/PointInTimeRestore.test.tsx @@ -50,18 +50,18 @@ describe("PointInTimeRestore", () => { jobName: "test-job", migrationType: CopyJobMigrationType.Offline, source: { - subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" }, + subscriptionId: "test-sub", account: mockSourceAccount, databaseId: "test-db", containerId: "test-container", }, target: { - subscriptionId: "test-sub", + subscription: { subscriptionId: "test-sub", displayName: "Test Subscription" }, account: mockSourceAccount, databaseId: "target-db", containerId: "target-container", }, - sourceReadAccessFromTarget: false, + sourceReadWriteAccessFromTarget: false, } as CopyJobContextState; const mockSetCopyJobState = jest.fn(); diff --git a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddManagedIdentity.test.tsx.snap b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddManagedIdentity.test.tsx.snap index 3454fad1e..8acceef4c 100644 --- a/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddManagedIdentity.test.tsx.snap +++ b/src/Explorer/ContainerCopy/CreateCopyJob/Screens/AssignPermissions/__snapshots__/AddManagedIdentity.test.tsx.snap @@ -204,7 +204,7 @@ exports[`AddManagedIdentity Snapshot Tests renders loading state 1`] = ` - Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. + Enable system-assigned managed identity on the source-account-name. To confirm, click the "Yes" button.
- Enable system-assigned managed identity on the test-target-account. To confirm, click the "Yes" button. + Enable system-assigned managed identity on the source-account-name. To confirm, click the "Yes" button.
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -63,7 +63,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
`; -exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Edge Cases should handle missing target account identity 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -126,7 +126,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Edge Cases should handle m
`; -exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when sourceReadAccessFromTarget is true 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when sourceReadWriteAccessFromTarget is true 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -189,7 +189,7 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
`; -exports[`AddReadPermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = ` +exports[`AddReadWritePermissionToDefaultIdentity Component Rendering should render correctly when toggle is on 1`] = `
- To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account. + To allow data copy from source to the destination container, provide read-write access on the destination account to the default identity of the source account.  
- Read permissions. + Read-write permissions.
@@ -255,12 +255,12 @@ exports[`AddReadPermissionToDefaultIdentity Component Rendering should render co
- Read permissions assigned to default identity. + Assign read-write permissions to default identity.
- Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button. + Assign read-write permissions on the destination account to the default identity of the source account. To confirm, click the "Yes" button.