Files
cosmos-explorer/test/sql/connectionStringLogin.spec.ts
T
Asier Isayas 3005ebd02d Add an E2E test that SQL connection string login skips the Portal Backend
Nothing asserted the defining behavior of the direct login path, so reverting the short-circuit in connectWithConnectionString would have gone unnoticed. The listener filters on the connectionstring route so it covers both generatetoken and accessinputmetadata, and leaves the account restriction check alone since that still runs for every API.
2026-08-19 16:01:41 -07:00

119 lines
5.3 KiB
TypeScript

import { Page, expect, test } from "@playwright/test";
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 databaseId = generateUniqueName("db");
const containerId = "testcontainer";
const documentId = "testdoc1";
async function loginWithConnectionString(page: Page, connectionString: string): Promise<void> {
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();
}
test.describe("SQL account using connection string login", () => {
let database: Database = null!;
let documentEndpoint: string = null!;
// SQL signs data-plane requests client-side with the account key, so no encrypted token is issued.
let connectionString: string = null!;
test.beforeAll("Seed Test Database", async () => {
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);
documentEndpoint = account.documentEndpoint!;
connectionString = `AccountEndpoint=${documentEndpoint};AccountKey=${keys.primaryMasterKey};`;
const client = new CosmosClient({ endpoint: 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 database?.delete();
});
test("reads a document after connection string login", async ({ page }) => {
await loginWithConnectionString(page, connectionString);
const explorer = await DataExplorer.waitForExplorer(page);
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(databaseId, containerId);
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("does not call the Portal Backend during login", async ({ page }) => {
// SQL derives the account metadata from the connection string and signs data-plane requests with the
// account key, so neither the encrypted token nor the access metadata endpoint should be hit.
const portalBackendCalls: string[] = [];
page.on("request", (request) => {
if (request.url().includes("/api/connectionstring/")) {
portalBackendCalls.push(request.url());
}
});
await loginWithConnectionString(page, connectionString);
const explorer = await DataExplorer.waitForExplorer(page);
const collectionNode = await explorer.waitForContainerNode(databaseId, containerId);
await expect(collectionNode.element).toBeAttached();
expect(portalBackendCalls).toEqual([]);
});
test("opens Data Explorer when the connection string has the wrong account key", async ({ page }) => {
// A well-formed but incorrect base64 account key. The login is accepted
// without checking the key against the account, so the user gets into Data Explorer either way and
// only the data-plane requests made from inside the explorer are rejected.
const wrongKey = "A".repeat(86) + "==";
await loginWithConnectionString(page, `AccountEndpoint=${documentEndpoint};AccountKey=${wrongKey};`);
await DataExplorer.waitForExplorer(page);
// The connect form is replaced by the explorer rather than staying up with a login error.
await expect(page.locator("#connectExplorer")).toHaveCount(0);
await expect(page.locator(".errorDetails")).toHaveCount(0);
});
});