mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-09-19 17:12:47 +01:00
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:
+40
-1
@@ -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<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 (
|
||||
process.env[`DE_TEST_ACCOUNT_NAME_${accountType.toLocaleUpperCase()}`] ??
|
||||
tryGetStandardName(accountType) ??
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user