Added more test cases and fix system partition key load issue (#2126)

* Added more test cases and fix system partition key load issue

* Fix unit tests and fix ci

* Updated test snapsho
This commit is contained in:
sunghyunkang1111 2025-04-30 15:18:11 -05:00 committed by GitHub
parent fe73d0a1c6
commit bb66deb3a4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 136 additions and 31 deletions

View File

@ -164,24 +164,24 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8] shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
shardTotal: [8] shardTotal: [16]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 18.x node-version: 18.x
- run: npm ci - run: npm ci
- run: npx playwright install --with-deps - run: npx playwright install --with-deps
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}} - name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts - name: Upload blob report to GitHub Actions Artifacts
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

@ -37,20 +37,51 @@ export default defineConfig({
}, },
{ {
name: "firefox", name: "firefox",
use: { ...devices["Desktop Firefox"] }, use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"security.fileuri.strict_origin_policy": false,
"network.http.referer.XOriginPolicy": 0,
"network.http.referer.trimmingPolicy": 0,
"privacy.file_unique_origin": false,
"security.csp.enable": false,
"network.cors_preflight.allow_client_cert": true,
"dom.security.https_first": false,
"network.http.cross-origin-embedder-policy": false,
"network.http.cross-origin-opener-policy": false,
"browser.tabs.remote.useCrossOriginPolicy": false,
"browser.tabs.remote.useCORP": false,
},
args: ["--disable-web-security"],
},
},
}, },
{ {
name: "webkit", name: "webkit",
use: { ...devices["Desktop Safari"] }, use: {
...devices["Desktop Safari"],
},
}, },
/* Test against branded browsers. */
{ {
name: "Google Chrome", name: "Google Chrome",
use: { ...devices["Desktop Chrome"], channel: "chrome" }, // or 'chrome-beta' use: {
...devices["Desktop Chrome"],
channel: "chrome",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
}, },
{ {
name: "Microsoft Edge", name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" }, // or 'msedge-dev' use: {
...devices["Desktop Edge"],
channel: "msedge",
launchOptions: {
args: ["--disable-web-security", "--disable-features=IsolateOrigins,site-per-process"],
},
},
}, },
], ],

View File

@ -193,6 +193,7 @@ export const InputDataList: FC<InputDataListProps> = ({
<> <>
<Input <Input
id="filterInput" id="filterInput"
data-test={"DocumentsTab/FilterInput"}
ref={inputRef} ref={inputRef}
type="text" type="text"
size="small" size="small"

View File

@ -773,8 +773,11 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
[_collection, _partitionKey], [_collection, _partitionKey],
); );
const partitionKeyPropertyHeaders: string[] = useMemo( const partitionKeyPropertyHeaders: string[] = useMemo(
() => (partitionKey?.systemKey ? [] : _collection?.partitionKeyPropertyHeaders || partitionKey?.paths), () =>
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey], isPreferredApiMongoDB && partitionKey?.systemKey
? []
: _collection?.partitionKeyPropertyHeaders || partitionKey?.paths,
[_collection?.partitionKeyPropertyHeaders, partitionKey?.paths, partitionKey?.systemKey, isPreferredApiMongoDB],
); );
let partitionKeyProperties = useMemo(() => { let partitionKeyProperties = useMemo(() => {
return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) => return partitionKeyPropertyHeaders?.map((partitionKeyPropertyHeader) =>
@ -2116,6 +2119,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
/> />
<Button <Button
appearance="primary" appearance="primary"
data-test={"DocumentsTab/ApplyFilter"}
size="small" size="small"
onClick={() => { onClick={() => {
if (isExecuting) { if (isExecuting) {
@ -2188,6 +2192,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
{tableItems.length > 0 && ( {tableItems.length > 0 && (
<a <a
className={styles.loadMore} className={styles.loadMore}
data-test={"DocumentsTab/LoadMore"}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => loadNextPage(documentsIterator.iterator, false)} onClick={() => loadNextPage(documentsIterator.iterator, false)}

View File

@ -51,6 +51,7 @@ exports[`Documents tab (noSql API) when rendered should render the page 1`] = `
<Button <Button
appearance="primary" appearance="primary"
aria-label="Apply filter" aria-label="Apply filter"
data-test="DocumentsTab/ApplyFilter"
disabled={false} disabled={false}
onClick={[Function]} onClick={[Function]}
size="small" size="small"

23
test/CORSBypass.ts Normal file
View File

@ -0,0 +1,23 @@
import { Page } from "@playwright/test";
export async function setupCORSBypass(page: Page) {
await page.route("**/api/mongo/explorer{,/**}", async (route) => {
const response = await route.fetch({
headers: {
...route.request().headers(),
},
});
await route.fulfill({
status: response.status(),
headers: {
...response.headers(),
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Credentials": "*",
},
body: await response.body(),
});
});
}

View File

@ -1,4 +1,4 @@
import { AzureCliCredential } from "@azure/identity"; import { DefaultAzureCredential } from "@azure/identity";
import { Frame, Locator, Page, expect } from "@playwright/test"; import { Frame, Locator, Page, expect } from "@playwright/test";
import crypto from "crypto"; import crypto from "crypto";
@ -20,8 +20,8 @@ export function generateUniqueName(baseName, options?: TestNameOptions): string
return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`; return `${prefix}${baseName}${crypto.randomBytes(length).toString("hex")}${suffix}`;
} }
export function getAzureCLICredentials(): AzureCliCredential { export function getAzureCLICredentials(): DefaultAzureCredential {
return new AzureCliCredential(); return new DefaultAzureCredential();
} }
export async function getAzureCLICredentialsToken(): Promise<string> { export async function getAzureCLICredentialsToken(): Promise<string> {
@ -223,6 +223,9 @@ export class DocumentsTab {
documentsListPane: Locator; documentsListPane: Locator;
documentResultsPane: Locator; documentResultsPane: Locator;
resultsEditor: Editor; resultsEditor: Editor;
loadMoreButton: Locator;
filterInput: Locator;
filterButton: Locator;
constructor( constructor(
public frame: Frame, public frame: Frame,
@ -234,6 +237,13 @@ export class DocumentsTab {
this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane"); this.documentsListPane = this.locator.getByTestId("DocumentsTab/DocumentsPane");
this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane"); this.documentResultsPane = this.locator.getByTestId("DocumentsTab/ResultsPane");
this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded")); this.resultsEditor = new Editor(this.frame, this.documentResultsPane.getByTestId("EditorReact/Host/Loaded"));
this.loadMoreButton = this.documentsListPane.getByTestId("DocumentsTab/LoadMore");
this.filterInput = this.documentsFilter.getByTestId("DocumentsTab/FilterInput");
this.filterButton = this.documentsFilter.getByTestId("DocumentsTab/ApplyFilter");
}
async setFilter(text: string) {
await this.filterInput.fill(text);
} }
} }

View File

@ -1,5 +1,6 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { setupCORSBypass } from "../CORSBypass";
import { DataExplorer, DocumentsTab, TestAccount } from "../fx"; import { DataExplorer, DocumentsTab, TestAccount } from "../fx";
import { retry, serializeMongoToJson, setPartitionKeys } from "../testData"; import { retry, serializeMongoToJson, setPartitionKeys } from "../testData";
import { documentTestCases } from "./testCases"; import { documentTestCases } from "./testCases";
@ -9,8 +10,9 @@ let documentsTab: DocumentsTab = null!;
for (const { name, databaseId, containerId, documents } of documentTestCases) { for (const { name, databaseId, containerId, documents } of documentTestCases) {
test.describe(`Test MongoRU Documents with ${name}`, () => { test.describe(`Test MongoRU Documents with ${name}`, () => {
test.skip(true, "Temporarily disabling all tests in this spec file"); // test.skip(true, "Temporarily disabling all tests in this spec file");
test.beforeEach("Open documents tab", async ({ page }) => { test.beforeEach("Open documents tab", async ({ page }) => {
await setupCORSBypass(page);
explorer = await DataExplorer.open(page, TestAccount.MongoReadonly); explorer = await DataExplorer.open(page, TestAccount.MongoReadonly);
const containerNode = await explorer.waitForContainerNode(databaseId, containerId); const containerNode = await explorer.waitForContainerNode(databaseId, containerId);
@ -25,6 +27,9 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
await documentsTab.documentsListPane.waitFor(); await documentsTab.documentsListPane.waitFor();
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });
}); });
test.afterEach(async ({ page }) => {
await page.unrouteAll({ behavior: "ignoreErrors" });
});
for (const document of documents) { for (const document of documents) {
const { documentId: docId, partitionKeys } = document; const { documentId: docId, partitionKeys } = document;
@ -68,8 +73,12 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
await documentsTab.resultsEditor.setText(JSON.stringify(newDocument)); await documentsTab.resultsEditor.setText(JSON.stringify(newDocument));
const saveButton = await explorer.waitForCommandBarButton("Save", 5000); const saveButton = await explorer.waitForCommandBarButton("Save", 5000);
await saveButton.click({ timeout: 5000 }); await saveButton.click({ timeout: 5000 });
await expect(saveButton).toBeHidden({ timeout: 5000 });
}, 3); }, 3);
await documentsTab.setFilter(`{_id: "${newDocumentId}"}`);
await documentsTab.filterButton.click();
const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0); const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
await newSpan.waitFor(); await newSpan.waitFor();
await newSpan.click(); await newSpan.click();

View File

@ -9,7 +9,7 @@ let documentsTab: DocumentsTab = null!;
for (const { name, databaseId, containerId, documents } of documentTestCases) { for (const { name, databaseId, containerId, documents } of documentTestCases) {
test.describe(`Test SQL Documents with ${name}`, () => { test.describe(`Test SQL Documents with ${name}`, () => {
test.skip(true, "Temporarily disabling all tests in this spec file"); // test.skip(true, "Temporarily disabling all tests in this spec file");
test.beforeEach("Open documents tab", async ({ page }) => { test.beforeEach("Open documents tab", async ({ page }) => {
explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly); explorer = await DataExplorer.open(page, TestAccount.SQLReadOnly);
@ -27,7 +27,7 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
}); });
for (const document of documents) { for (const document of documents) {
const { documentId: docId, partitionKeys } = document; const { documentId: docId, partitionKeys, skipCreateDelete } = document;
test.describe(`Document ID: ${docId}`, () => { test.describe(`Document ID: ${docId}`, () => {
test(`should load and view document ${docId}`, async () => { test(`should load and view document ${docId}`, async () => {
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0); const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
@ -42,7 +42,9 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
expect(resultText).not.toBeNull(); expect(resultText).not.toBeNull();
expect(resultData?.id).toEqual(docId); expect(resultData?.id).toEqual(docId);
}); });
test(`should be able to create and delete new document from ${docId}`, async ({ page }) => {
const testOrSkip = skipCreateDelete ? test.skip : test;
testOrSkip(`should be able to create and delete new document from ${docId}`, async ({ page }) => {
const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0); const span = documentsTab.documentsListPane.getByText(docId, { exact: true }).nth(0);
await span.waitFor(); await span.waitFor();
await expect(span).toBeVisible(); await expect(span).toBeVisible();
@ -51,10 +53,6 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
let newDocumentId; let newDocumentId;
await page.waitForTimeout(5000); await page.waitForTimeout(5000);
await retry(async () => { await retry(async () => {
// const discardButton = await explorer.waitForCommandBarButton("Discard", 5000);
// if (await discardButton.isEnabled()) {
// await discardButton.click();
// }
const newDocumentButton = await explorer.waitForCommandBarButton("New Item", 5000); const newDocumentButton = await explorer.waitForCommandBarButton("New Item", 5000);
await expect(newDocumentButton).toBeVisible(); await expect(newDocumentButton).toBeVisible();
await expect(newDocumentButton).toBeEnabled(); await expect(newDocumentButton).toBeEnabled();
@ -72,10 +70,15 @@ for (const { name, databaseId, containerId, documents } of documentTestCases) {
await documentsTab.resultsEditor.setText(JSON.stringify(newDocument)); await documentsTab.resultsEditor.setText(JSON.stringify(newDocument));
const saveButton = await explorer.waitForCommandBarButton("Save", 5000); const saveButton = await explorer.waitForCommandBarButton("Save", 5000);
await saveButton.click({ timeout: 5000 }); await saveButton.click({ timeout: 5000 });
await expect(saveButton).toBeHidden({ timeout: 5000 });
}, 3); }, 3);
await documentsTab.setFilter(`WHERE c.id = "${newDocumentId}"`);
await documentsTab.filterButton.click();
const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0); const newSpan = documentsTab.documentsListPane.getByText(newDocumentId, { exact: true }).nth(0);
await newSpan.waitFor(); await newSpan.waitFor();
await newSpan.click(); await newSpan.click();
await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 }); await expect(documentsTab.resultsEditor.locator).toBeAttached({ timeout: 60 * 1000 });

View File

@ -5,7 +5,24 @@ export const documentTestCases: DocumentTestCase[] = [
name: "System Partition Key", name: "System Partition Key",
databaseId: "e2etests-sql-readonly", databaseId: "e2etests-sql-readonly",
containerId: "systemPartitionKey", containerId: "systemPartitionKey",
documents: [{ documentId: "systempartition", partitionKeys: [] }], documents: [
{
documentId: "systempartition",
partitionKeys: [{ key: "/_partitionKey", value: "partitionKey" }],
skipCreateDelete: true,
},
{
documentId: "systempartition_empty",
partitionKeys: [{ key: "/_partitionKey", value: "" }],
skipCreateDelete: true,
},
{
documentId: "systempartition_null",
partitionKeys: [{ key: "/_partitionKey", value: null }],
skipCreateDelete: true,
},
{ documentId: "systempartition_missing", partitionKeys: [] },
],
}, },
{ {
name: "Single Partition Key", name: "Single Partition Key",

View File

@ -1,13 +1,16 @@
import crypto from "crypto";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb"; import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import { BulkOperationType, Container, CosmosClient, Database, JSONObject } from "@azure/cosmos"; import { BulkOperationType, Container, CosmosClient, Database, JSONObject } from "@azure/cosmos";
import crypto from "crypto"; import { AzureIdentityCredentialAdapter } from "@azure/ms-rest-js";
import { import {
TestAccount,
generateUniqueName, generateUniqueName,
getAccountName, getAccountName,
getAzureCLICredentials, getAzureCLICredentials,
resourceGroupName, resourceGroupName,
subscriptionId, subscriptionId,
TestAccount,
} from "./fx"; } from "./fx";
export interface TestItem { export interface TestItem {
@ -26,6 +29,7 @@ export interface DocumentTestCase {
export interface TestDocument { export interface TestDocument {
documentId: string; documentId: string;
partitionKeys?: PartitionKey[]; partitionKeys?: PartitionKey[];
skipCreateDelete?: boolean;
} }
export interface PartitionKey { export interface PartitionKey {
@ -74,7 +78,8 @@ export async function createTestSQLContainer(includeTestData?: boolean) {
const databaseId = generateUniqueName("db"); const databaseId = generateUniqueName("db");
const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique const containerId = "testcontainer"; // A unique container name isn't needed because the database is unique
const credentials = getAzureCLICredentials(); const credentials = getAzureCLICredentials();
const armClient = new CosmosDBManagementClient(credentials, subscriptionId); const adaptedCredentials = new AzureIdentityCredentialAdapter(credentials);
const armClient = new CosmosDBManagementClient(adaptedCredentials, subscriptionId);
const accountName = getAccountName(TestAccount.SQL); const accountName = getAccountName(TestAccount.SQL);
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);