Add E2E connection string login tests for SQL, Gremlin, and Tables

This commit is contained in:
Asier Isayas
2026-08-12 16:42:11 -04:00
parent f447974f0a
commit cd1b8da36e
3 changed files with 270 additions and 0 deletions
@@ -0,0 +1,90 @@
import { expect, test } from "@playwright/test";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { CosmosClient, Database } from "@azure/cosmos";
import {
DataExplorer,
Editor,
ONE_MINUTE_MS,
TestAccount,
generateUniqueName,
getAccountName,
getAzureCLICredentials,
resourceGroupName,
subscriptionId,
} from "../fx";
const gremlinAccountRbacToken = process.env.GREMLIN_TESTACCOUNT_TOKEN ?? "";
const databaseId = generateUniqueName("db");
const graphId = "testgraph";
const vertexId = "testvertex";
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 account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
// Gremlin graphs are stored as documents, so seed a vertex via the SQL client using Cosmos' internal graph format.
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: graphId,
partitionKey: { paths: ["/pk"] },
});
await container.items.upsert({ id: vertexId, label: "person", pk: "pk1" });
});
test.afterAll("Delete Test Database", async () => {
await database?.delete();
});
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 account = await armClient.databaseAccounts.get(resourceGroupName, accountName);
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, accountName);
// Gremlin signs data-plane requests client-side with the account key, so no encrypted token is issued.
const connectionString = `AccountEndpoint=${account.documentEndpoint};AccountKey=${keys.primaryMasterKey};ApiKind=Gremlin;`;
await page.goto("https://localhost:1234/hostedExplorer.html");
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
await switchConnectionLink.waitFor();
await switchConnectionLink.click();
await page.getByPlaceholder("Please enter a connection string").fill(connectionString);
await page.getByRole("button", { name: "Connect" }).click();
const explorer = await DataExplorer.waitForExplorer(page);
const graphNode = await explorer.waitForContainerNode(databaseId, graphId);
await graphNode.expand();
// Open the Graph node to load the graph explorer, then run the default query to read the seeded vertex.
const graphDataNode = await explorer.waitForNode(`${databaseId}/${graphId}/Graph`);
await graphDataNode.element.click();
await explorer.frame.getByRole("button", { name: "Execute Gremlin Query" }).click();
// Results open in the Graph view; switch to the JSON view to read the vertex document.
const jsonResultsTab = explorer.frame.getByRole("tab", { name: "JSON" });
await jsonResultsTab.waitFor({ timeout: ONE_MINUTE_MS });
await jsonResultsTab.click();
const graphJsonEditor = new Editor(
explorer.frame,
explorer.frame.locator(".graphJsonEditor").getByTestId("EditorReact/Host/Loaded"),
);
await expect.poll(async () => await graphJsonEditor.text(), { timeout: ONE_MINUTE_MS }).toContain(vertexId);
});
});
+91
View File
@@ -0,0 +1,91 @@
import { expect, test } from "@playwright/test";
import { DataExplorer, getAccountName, ONE_MINUTE_MS, resourceGroupName, TestAccount } from "../fx";
import { createTestSQLContainer, TestContainerContext } from "../testData";
const nosqlAccountRbacToken = process.env.NOSQL_TESTACCOUNT_TOKEN ?? "";
const documentId = "testdoc1";
test.describe("SQL account using connection string login", () => {
let context: TestContainerContext = null!;
test.beforeAll("Seed Test Database", async () => {
if (nosqlAccountRbacToken.length > 0) {
return;
}
context = await createTestSQLContainer({ partitionKey: "/id" });
await context.container.items.upsert({ id: documentId });
});
test.afterAll("Delete Test Database", async () => {
await context?.dispose();
});
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);
// 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};`;
await page.goto("https://localhost:1234/hostedExplorer.html");
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
await switchConnectionLink.waitFor();
await switchConnectionLink.click();
await page.getByPlaceholder("Please enter a connection string").fill(connectionString);
await page.getByRole("button", { name: "Connect" }).click();
const explorer = await DataExplorer.waitForExplorer(page);
const collectionNode = await explorer.waitForContainerNode(context.database.id, context.container.id);
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);
await itemsNode.element.click();
const documentsTab = explorer.documentsTab("tab0");
await documentsTab.documentsFilter.waitFor();
await documentsTab.documentsListPane.waitFor();
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: ONE_MINUTE_MS });
const documentRow = documentsTab.documentsListPane.getByText(documentId, { exact: true }).nth(0);
await documentRow.waitFor();
await documentRow.click();
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: ONE_MINUTE_MS });
const resultText = await documentsTab.resultsEditor.text();
expect(resultText).not.toBeNull();
const resultData = JSON.parse(resultText!);
expect(resultData?.id).toEqual(documentId);
});
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);
// A well-formed but incorrect base64 account key: the endpoint is valid, so the Cosmos client reaches
// the account but the data-plane request is rejected with 401 Unauthorized.
const wrongKey = Buffer.alloc(64).toString("base64");
const connectionString = `AccountEndpoint=${account.documentEndpoint};AccountKey=${wrongKey};`;
await page.goto("https://localhost:1234/hostedExplorer.html");
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
await switchConnectionLink.waitFor();
await switchConnectionLink.click();
await page.getByPlaceholder("Please enter a connection string").fill(connectionString);
await page.getByRole("button", { name: "Connect" }).click();
// The connect form stays visible and surfaces the connectivity error instead of opening the explorer.
await expect(page.locator(".errorDetails")).toContainText("Unable to connect to the account", {
timeout: ONE_MINUTE_MS,
});
});
});
+89
View File
@@ -0,0 +1,89 @@
import { expect, test } from "@playwright/test";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { Container, CosmosClient } from "@azure/cosmos";
import {
DataExplorer,
ONE_MINUTE_MS,
TestAccount,
generateUniqueName,
getAccountName,
getAzureCLICredentials,
resourceGroupName,
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");
const partitionKey = "testpartition";
const rowKey = "testrow";
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 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 });
const { database } = await client.databases.createIfNotExists({ id: databaseId });
container = (
await database.containers.createIfNotExists({
id: tableId,
partitionKey: { paths: ["/'$pk'"] },
})
).container;
await container.items.upsert({ $pk: partitionKey, id: rowKey, $id: rowKey });
});
test.afterAll("Delete Test Table", async () => {
// Only remove the table we created; the fixed "TablesDB" database is shared by every table in the
// account, so deleting it would destroy unrelated tables.
await container?.delete();
});
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 { connectionStrings = [] } = await armClient.databaseAccounts.listConnectionStrings(
resourceGroupName,
accountName,
);
// Tables sign data-plane requests client-side with the account key, so no encrypted token is issued.
const connectionString = connectionStrings.find((cs) => cs.type === "Table")?.connectionString;
await page.goto("https://localhost:1234/hostedExplorer.html");
const switchConnectionLink = page.getByTestId("Link:SwitchConnectionType");
await switchConnectionLink.waitFor();
await switchConnectionLink.click();
await page.getByPlaceholder("Please enter a connection string").fill(connectionString!);
await page.getByRole("button", { name: "Connect" }).click();
const explorer = await DataExplorer.waitForExplorer(page);
const tableNode = await explorer.waitForContainerNode(databaseId, tableId);
await tableNode.expand();
// Open the Entities node to load the table entities grid and read the seeded entity through the data plane.
const entitiesNode = await explorer.waitForNode(`${databaseId}/${tableId}/Entities`);
await entitiesNode.element.click();
const entitiesGrid = explorer.frame.locator("#storageTable");
await expect(entitiesGrid).toBeVisible({ timeout: ONE_MINUTE_MS });
await expect(entitiesGrid.getByText(rowKey, { exact: true }).first()).toBeVisible({ timeout: ONE_MINUTE_MS });
await expect(entitiesGrid.getByText(partitionKey, { exact: true }).first()).toBeVisible();
});
});