diff --git a/test/fx.ts b/test/fx.ts index 880388a33..cbaca0dfc 100644 --- a/test/fx.ts +++ b/test/fx.ts @@ -41,6 +41,14 @@ export enum TestAccount { SQL = "SQL", SQLReadOnly = "SQLReadOnly", SQLContainerCopyOnly = "SQLContainerCopyOnly", + SQLConnectionString = "SQLConnectionString", + TableConnectionString = "TableConnectionString", + GremlinConnectionString = "GremlinConnectionString", +} + +export enum TestAuthType { + EntraID = "EntraID", + ConnectionString = "ConnectionString", } export function getDefaultAccountName(accountType: TestAccount): string { @@ -66,6 +74,12 @@ export function getDefaultAccountName(accountType: TestAccount): string { return `${accountNamePrefix}-de-test-sql-readonly`; case TestAccount.SQLContainerCopyOnly: return `${accountNamePrefix}-de-test-sql-containercopy`; + case TestAccount.SQLConnectionString: + return `${accountNamePrefix}-de-test-sql-connstring-1`; + case TestAccount.TableConnectionString: + return `${accountNamePrefix}-de-test-table-connstring-1`; + case TestAccount.GremlinConnectionString: + return `${accountNamePrefix}-de-test-gremlin-connstring-1`; case TestAccount.SQL: { const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? ""; if (!shardIndex) { @@ -96,7 +110,32 @@ function tryGetStandardName(accountType: TestAccount) { } } -export function getAccountName(accountType: TestAccount) { +// Maps a base API account type to its dedicated connection string (account key) account. +const connectionStringAccountTypes: Partial> = { + [TestAccount.SQL]: TestAccount.SQLConnectionString, + [TestAccount.Tables]: TestAccount.TableConnectionString, + [TestAccount.Gremlin]: TestAccount.GremlinConnectionString, +}; + +export function getAccountName(accountType: TestAccount, authType: TestAuthType = TestAuthType.EntraID): string { + // Connection string (account key) login uses dedicated *-connstring accounts that are only + // provisioned in CI (resolved via DE_ACCOUNT_PREFIX). Local runs use DE_TEST_ACCOUNT_PREFIX and + // typically don't have those accounts, so they fall back to the standard API account for the same + // API (which also has key auth enabled). + if (authType === TestAuthType.ConnectionString) { + const connectionStringType = connectionStringAccountTypes[accountType]; + if (!connectionStringType) { + throw new Error(`No connection string account defined for account type ${accountType}`); + } + const override = process.env[`DE_TEST_ACCOUNT_NAME_${connectionStringType.toLocaleUpperCase()}`]; + if (override) { + return override; + } + if (!process.env.DE_TEST_ACCOUNT_PREFIX) { + return getAccountName(connectionStringType); + } + } + return ( process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? tryGetStandardName(accountType) ?? diff --git a/test/gremlin/connectionStringLogin.spec.ts b/test/gremlin/connectionStringLogin.spec.ts index f404693c7..50289d867 100644 --- a/test/gremlin/connectionStringLogin.spec.ts +++ b/test/gremlin/connectionStringLogin.spec.ts @@ -7,6 +7,7 @@ import { Editor, ONE_MINUTE_MS, TestAccount, + TestAuthType, generateUniqueName, getAccountName, getAzureCLICredentials, @@ -14,7 +15,6 @@ import { subscriptionId, } from "../fx"; -const gremlinAccountRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN ?? ""; const databaseId = generateUniqueName("db"); const graphId = "testgraph"; const vertexId = "testvertex"; @@ -23,12 +23,9 @@ test.describe("Gremlin account using connection string login", () => { let database: Database = null!; test.beforeAll("Seed Test Database", async () => { - if (gremlinAccountRbacToken.length > 0) { - return; - } const credentials = getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); - const accountName = getAccountName(TestAccount.Gremlin); + const accountName = getAccountName(TestAccount.Gremlin, TestAuthType.ConnectionString); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); @@ -47,12 +44,9 @@ test.describe("Gremlin account using connection string login", () => { }); test("reads a vertex after connection string login", async ({ page }) => { - // Connection string (account key) login is not supported when local auth is disabled for data plane RBAC. - test.skip(gremlinAccountRbacToken.length > 0); - const credentials = getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); - const accountName = getAccountName(TestAccount.Gremlin); + const accountName = getAccountName(TestAccount.Gremlin, TestAuthType.ConnectionString); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); diff --git a/test/sql/connectionStringLogin.spec.ts b/test/sql/connectionStringLogin.spec.ts index c24fc239e..880e84cb8 100644 --- a/test/sql/connectionStringLogin.spec.ts +++ b/test/sql/connectionStringLogin.spec.ts @@ -1,33 +1,52 @@ import { expect, test } from "@playwright/test"; -import { DataExplorer, getAccountName, ONE_MINUTE_MS, resourceGroupName, TestAccount } from "../fx"; -import { createTestSQLContainer, TestContainerContext } from "../testData"; +import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; +import { CosmosClient, Database } from "@azure/cosmos"; +import { + DataExplorer, + ONE_MINUTE_MS, + TestAccount, + TestAuthType, + generateUniqueName, + getAccountName, + getAzureCLICredentials, + resourceGroupName, + subscriptionId, +} from "../fx"; -const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN ?? ""; +const databaseId = generateUniqueName("db"); +const containerId = "testcontainer"; const documentId = "testdoc1"; test.describe("SQL account using connection string login", () => { - let context: TestContainerContext = null!; + let database: Database = null!; test.beforeAll("Seed Test Database", async () => { - if (nosqlAccountRbacToken.length > 0) { - return; - } - context = await createTestSQLContainer({ partitionKey: "/id" }); - await context.container.items.upsert({ id: documentId }); + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + + const client = new CosmosClient({ endpoint: account.documentEndpoint!, key: keys.primaryMasterKey }); + database = (await client.databases.createIfNotExists({ id: databaseId })).database; + const { container } = await database.containers.createIfNotExists({ + id: containerId, + partitionKey: { paths: ["/id"] }, + }); + await container.items.upsert({ id: documentId }); }); test.afterAll("Delete Test Database", async () => { - await context?.dispose(); + await database?.delete(); }); test("reads a document after connection string login", async ({ page }) => { - // Connection string (account key) login is not supported when local auth is disabled for data plane RBAC. - test.skip(nosqlAccountRbacToken.length > 0); - - const accountName = getAccountName(TestAccount.SQL); - const account = await context.armClient.databaseAccounts.get(resourceGroupName, accountName); - const keys = await context.armClient.databaseAccounts.listKeys(resourceGroupName, accountName); + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); + const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); // SQL signs data-plane requests client-side with the account key, so no encrypted token is issued. const connectionString = `AccountEndpoint=${account.documentEndpoint};AccountKey=${keys.primaryMasterKey};`; @@ -40,12 +59,12 @@ test.describe("SQL account using connection string login", () => { await page.getByRole("button", { name: "Connect" }).click(); const explorer = await DataExplorer.waitForExplorer(page); - const collectionNode = await explorer.waitForContainerNode(context.database.id, context.container.id); + const collectionNode = await explorer.waitForContainerNode(databaseId, containerId); await expect(collectionNode.element).toBeAttached(); await collectionNode.expand(); // Open the Items node to load the Documents tab and read the seeded document through the data plane. - const itemsNode = await explorer.waitForContainerItemsNode(context.database.id, context.container.id); + const itemsNode = await explorer.waitForContainerItemsNode(databaseId, containerId); await itemsNode.element.click(); const documentsTab = explorer.documentsTab("tab0"); @@ -65,11 +84,10 @@ test.describe("SQL account using connection string login", () => { }); test("shows an error when the connection string has the wrong account key", async ({ page }) => { - // Connection string (account key) login is not supported when local auth is disabled for data plane RBAC. - test.skip(nosqlAccountRbacToken.length > 0); - - const accountName = getAccountName(TestAccount.SQL); - const account = await context.armClient.databaseAccounts.get(resourceGroupName, accountName); + const credentials = getAzureCLICredentials(); + const armClient = new CosmosDBManagementClient(credentials, subscriptionId); + const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString); + const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); // A well-formed but incorrect base64 account key (88-char, 64-byte): the endpoint is valid, so the // Cosmos client reaches the account but the data-plane request is rejected with 401 Unauthorized. diff --git a/test/tables/connectionStringLogin.spec.ts b/test/tables/connectionStringLogin.spec.ts index 132382457..7444ae212 100644 --- a/test/tables/connectionStringLogin.spec.ts +++ b/test/tables/connectionStringLogin.spec.ts @@ -6,6 +6,7 @@ import { DataExplorer, ONE_MINUTE_MS, TestAccount, + TestAuthType, generateUniqueName, getAccountName, getAzureCLICredentials, @@ -13,8 +14,6 @@ import { subscriptionId, } from "../fx"; -const tableAccountRbacToken = process.env.TABLE_TESTACCOUNT_TOKEN ?? ""; - // Tables API accounts store tables in a fixed "TablesDB" database, with each table as a container. const databaseId = "TablesDB"; const tableId = generateUniqueName("table"); @@ -25,12 +24,9 @@ test.describe("Tables account using connection string login", () => { let container: Container = null!; test.beforeAll("Seed Test Table", async () => { - if (tableAccountRbacToken.length > 0) { - return; - } const credentials = getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); - const accountName = getAccountName(TestAccount.Tables); + const accountName = getAccountName(TestAccount.Tables, TestAuthType.ConnectionString); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); @@ -52,12 +48,9 @@ test.describe("Tables account using connection string login", () => { }); test("reads an entity after connection string login", async ({ page }) => { - // Connection string (account key) login is not supported when local auth is disabled for data plane RBAC. - test.skip(tableAccountRbacToken.length > 0); - const credentials = getAzureCLICredentials(); const armClient = new CosmosDBManagementClient(credentials, subscriptionId); - const accountName = getAccountName(TestAccount.Tables); + const accountName = getAccountName(TestAccount.Tables, TestAuthType.ConnectionString); const { connectionStrings = [] } = await armClient.databaseAccounts.listConnectionStrings( resourceGroupName, accountName,