Wire connection string login E2E tests to dedicated connstring accounts in CI

Add TestAuthType and fold connection-string account resolution into getAccountName; seed and target the dedicated *-connstring accounts in CI while falling back to the standard per-API account locally.
This commit is contained in:
Asier Isayas
2026-08-13 15:02:42 -04:00
parent 1ec46eade8
commit d92248024a
4 changed files with 87 additions and 43 deletions
+40 -1
View File
@@ -41,6 +41,14 @@ export enum TestAccount {
SQL = "SQL", SQL = "SQL",
SQLReadOnly = "SQLReadOnly", SQLReadOnly = "SQLReadOnly",
SQLContainerCopyOnly = "SQLContainerCopyOnly", SQLContainerCopyOnly = "SQLContainerCopyOnly",
SQLConnectionString = "SQLConnectionString",
TableConnectionString = "TableConnectionString",
GremlinConnectionString = "GremlinConnectionString",
}
export enum TestAuthType {
EntraID = "EntraID",
ConnectionString = "ConnectionString",
} }
export function getDefaultAccountName(accountType: TestAccount): string { export function getDefaultAccountName(accountType: TestAccount): string {
@@ -66,6 +74,12 @@ export function getDefaultAccountName(accountType: TestAccount): string {
return `${accountNamePrefix}-de-test-sql-readonly`; return `${accountNamePrefix}-de-test-sql-readonly`;
case TestAccount.SQLContainerCopyOnly: case TestAccount.SQLContainerCopyOnly:
return `${accountNamePrefix}-de-test-sql-containercopy`; 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: { case TestAccount.SQL: {
const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? ""; const shardIndex = process.env.PLAYWRIGHT_SHARD_INDEX ?? "";
if (!shardIndex) { 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<Record<TestAccount, TestAccount>> = {
[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 ( return (
process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ?? process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ??
tryGetStandardName(accountType) ?? tryGetStandardName(accountType) ??
+3 -9
View File
@@ -7,6 +7,7 @@ import {
Editor, Editor,
ONE_MINUTE_MS, ONE_MINUTE_MS,
TestAccount, TestAccount,
TestAuthType,
generateUniqueName, generateUniqueName,
getAccountName, getAccountName,
getAzureCLICredentials, getAzureCLICredentials,
@@ -14,7 +15,6 @@ import {
subscriptionId, subscriptionId,
} from "../fx"; } from "../fx";
const gremlinAccountRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN ?? "";
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const graphId = "testgraph"; const graphId = "testgraph";
const vertexId = "testvertex"; const vertexId = "testvertex";
@@ -23,12 +23,9 @@ test.describe("Gremlin account using connection string login", () => {
let database: Database = null!; let database: Database = null!;
test.beforeAll("Seed Test Database", async () => { test.beforeAll("Seed Test Database", async () => {
if (gremlinAccountRbacToken.length > 0) {
return;
}
const credentials = getAzureCLICredentials(); const credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); 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 account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(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 }) => { 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 credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); 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 account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
+41 -23
View File
@@ -1,33 +1,52 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { DataExplorer, getAccountName, ONE_MINUTE_MS, resourceGroupName, TestAccount } from "../fx"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { createTestSQLContainer, TestContainerContext } from "../testData"; 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"; const documentId = "testdoc1";
test.describe("SQL account using connection string login", () => { test.describe("SQL account using connection string login", () => {
let context: TestContainerContext = null!; let database: Database = null!;
test.beforeAll("Seed Test Database", async () => { test.beforeAll("Seed Test Database", async () => {
if (nosqlAccountRbacToken.length > 0) { const credentials = getAzureCLICredentials();
return; const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
} const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString);
context = await createTestSQLContainer({ partitionKey: "/id" }); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
await context.container.items.upsert({ id: documentId }); 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 () => { test.afterAll("Delete Test Database", async () => {
await context?.dispose(); await database?.delete();
}); });
test("reads a document after connection string login", async ({ page }) => { 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. const credentials = getAzureCLICredentials();
test.skip(nosqlAccountRbacToken.length > 0); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString);
const accountName = getAccountName(TestAccount.SQL); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const account = await context.armClient.databaseAccounts.get(resourceGroupName, accountName); const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
const keys = await context.armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
// SQL signs data-plane requests client-side with the account key, so no encrypted token is issued. // 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};`; 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(); await page.getByRole("button", { name: "Connect" }).click();
const explorer = await DataExplorer.waitForExplorer(page); 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 expect(collectionNode.element).toBeAttached();
await collectionNode.expand(); await collectionNode.expand();
// Open the Items node to load the Documents tab and read the seeded document through the data plane. // 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(); await itemsNode.element.click();
const documentsTab = explorer.documentsTab("tab0"); 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 }) => { 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. const credentials = getAzureCLICredentials();
test.skip(nosqlAccountRbacToken.length > 0); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL, TestAuthType.ConnectionString);
const accountName = getAccountName(TestAccount.SQL); const account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const account = await context.armClient.databaseAccounts.get(resourceGroupName, accountName);
// A well-formed but incorrect base64 account key (88-char, 64-byte): the endpoint is valid, so the // 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. // Cosmos client reaches the account but the data-plane request is rejected with 401 Unauthorized.
+3 -10
View File
@@ -6,6 +6,7 @@ import {
DataExplorer, DataExplorer,
ONE_MINUTE_MS, ONE_MINUTE_MS,
TestAccount, TestAccount,
TestAuthType,
generateUniqueName, generateUniqueName,
getAccountName, getAccountName,
getAzureCLICredentials, getAzureCLICredentials,
@@ -13,8 +14,6 @@ import {
subscriptionId, subscriptionId,
} from "../fx"; } 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. // Tables API accounts store tables in a fixed "TablesDB" database, with each table as a container.
const databaseId = "TablesDB"; const databaseId = "TablesDB";
const tableId = generateUniqueName("table"); const tableId = generateUniqueName("table");
@@ -25,12 +24,9 @@ test.describe("Tables account using connection string login", () => {
let container: Container = null!; let container: Container = null!;
test.beforeAll("Seed Test Table", async () => { test.beforeAll("Seed Test Table", async () => {
if (tableAccountRbacToken.length > 0) {
return;
}
const credentials = getAzureCLICredentials(); const credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); 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 account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(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 }) => { 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 credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const accountName = getAccountName(TestAccount.Tables); const accountName = getAccountName(TestAccount.Tables, TestAuthType.ConnectionString);
const { connectionStrings = [] } = await armClient.databaseAccounts.listConnectionStrings( const { connectionStrings = [] } = await armClient.databaseAccounts.listConnectionStrings(
resourceGroupName, resourceGroupName,
accountName, accountName,