From 876b531248d6c9593287ac158ed8b102f1f5f69f Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Tue, 30 Dec 2025 22:54:42 -0800 Subject: [PATCH 1/7] Add changes for Load more option to work --- src/Common/Constants.ts | 2 +- src/Common/MongoProxyClient.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 65b3f8d51..06eb3a165 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -160,7 +160,7 @@ export class PortalBackendEndpoints { export class MongoProxyEndpoints { public static readonly Development: string = "https://localhost:7238"; - public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; + public static readonly Mpac: string = "https://localhost:7238"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; diff --git a/src/Common/MongoProxyClient.ts b/src/Common/MongoProxyClient.ts index 48eef3a48..b4287e0ee 100644 --- a/src/Common/MongoProxyClient.ts +++ b/src/Common/MongoProxyClient.ts @@ -38,7 +38,7 @@ export function queryIterator(databaseId: string, collection: Collection, query: let continuationToken: string; return { fetchNext: () => { - return queryDocuments(databaseId, collection, false, query).then((response) => { + return queryDocuments(databaseId, collection, false, query, continuationToken).then((response) => { continuationToken = response.continuationToken; const headers: { [key: string]: string | number } = {}; response.headers.forEach((value, key) => { From 9dad75c2f95f0355aac153aad40a67b2a9fb9742 Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Tue, 30 Dec 2025 23:21:56 -0800 Subject: [PATCH 2/7] Remove localhost --- src/Common/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 06eb3a165..65b3f8d51 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -160,7 +160,7 @@ export class PortalBackendEndpoints { export class MongoProxyEndpoints { public static readonly Development: string = "https://localhost:7238"; - public static readonly Mpac: string = "https://localhost:7238"; + public static readonly Mpac: string = "https://cdb-ms-mpac-mp.cosmos.azure.com"; public static readonly Prod: string = "https://cdb-ms-prod-mp.cosmos.azure.com"; public static readonly Fairfax: string = "https://cdb-ff-prod-mp.cosmos.azure.us"; public static readonly Mooncake: string = "https://cdb-mc-prod-mp.cosmos.azure.cn"; From 50a244e6f9714ee355d69370f562d832c4f6239d Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Thu, 8 Jan 2026 12:55:24 -0800 Subject: [PATCH 3/7] Add Mongo Pagination tests --- test/mongo/pagination.spec.ts | 128 ++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 test/mongo/pagination.spec.ts diff --git a/test/mongo/pagination.spec.ts b/test/mongo/pagination.spec.ts new file mode 100644 index 000000000..b7d0d997c --- /dev/null +++ b/test/mongo/pagination.spec.ts @@ -0,0 +1,128 @@ +import { expect, test } from "@playwright/test"; +import { setupCORSBypass } from "../CORSBypass"; +import { DataExplorer, QueryTab, TestAccount, CommandBarButton, Editor } from "../fx"; +import { serializeMongoToJson } from "../testData"; + +const databaseId = "test-e2etests-mongo-pagination"; +const collectionId = "test-coll-mongo-pagination"; +let explorer: DataExplorer = null!; + +test.setTimeout(5 * 60 * 1000); + +test.describe("Test Mongo Pagination", () => { + let queryTab: QueryTab; + let queryEditor: Editor; + + test.beforeEach("Open query tab", async ({ page }) => { + await setupCORSBypass(page); + explorer = await DataExplorer.open(page, TestAccount.MongoReadonly); + + const containerNode = await explorer.waitForContainerNode(databaseId, collectionId); + await containerNode.expand(); + + const containerMenuNode = await explorer.waitForContainerDocumentsNode(databaseId, collectionId); + await containerMenuNode.openContextMenu(); + await containerMenuNode.contextMenuItem("New Query").click(); + + queryTab = explorer.queryTab("tab0"); + queryEditor = queryTab.editor(); + await queryEditor.locator.waitFor({ timeout: 30 * 1000 }); + await queryTab.executeCTA.waitFor(); + await explorer.frame.getByTestId("NotificationConsole/ExpandCollapseButton").click(); + await explorer.frame.getByTestId("NotificationConsole/Contents").waitFor(); + }); + + test("should execute a query and load more results", async ({ page }) => { + const query = "{}"; + + await queryEditor.locator.click(); + await queryEditor.setText(query); + + const executeQueryButton = explorer.commandBarButton(CommandBarButton.ExecuteQuery); + await executeQueryButton.click(); + + // Wait for query execution to complete + await expect(queryTab.resultsView).toBeVisible({ timeout: 60000 }); + await expect(queryTab.resultsEditor.locator).toBeAttached({ timeout: 30000 }); + + // Get initial results + const resultText = await queryTab.resultsEditor.text(); + + if (!resultText || resultText.trim() === '' || resultText.trim() === '[]') { + throw new Error("Query returned no results - the collection appears to be empty"); + } + + const resultData = serializeMongoToJson(resultText); + + if (resultData.length === 0) { + throw new Error("Parsed results contain 0 documents - collection is empty"); + } + + if (resultData.length < 100) { + expect(resultData.length).toBeGreaterThan(0); + return; + } + + expect(resultData.length).toBe(100); + + // Pagination test + let totalPagesLoaded = 1; + const maxLoadMoreAttempts = 10; + + for (let loadMoreAttempts = 0; loadMoreAttempts < maxLoadMoreAttempts; loadMoreAttempts++) { + const loadMoreButton = queryTab.resultsView.getByText("Load more"); + + try { + await expect(loadMoreButton).toBeVisible({ timeout: 5000 }); + } catch { + // Load more button not visible - pagination complete + break; + } + + const beforeClickText = await queryTab.resultsEditor.text(); + const beforeClickHash = Buffer.from(beforeClickText || "").toString('base64').substring(0, 50); + + await loadMoreButton.click(); + + // Wait for content to update + let editorContentChanged = false; + for (let waitAttempt = 1; waitAttempt <= 3; waitAttempt++) { + await page.waitForTimeout(2000); + + const currentEditorText = await queryTab.resultsEditor.text(); + const currentHash = Buffer.from(currentEditorText || "").toString('base64').substring(0, 50); + + if (currentHash !== beforeClickHash) { + editorContentChanged = true; + break; + } + } + + if (editorContentChanged) { + totalPagesLoaded++; + } else { + // No content change detected, stop pagination + break; + } + + await page.waitForTimeout(1000); + } + + // Final verification + const finalIndicator = queryTab.resultsView.locator('text=/\\d+ - \\d+/'); + const finalIndicatorText = await finalIndicator.textContent(); + + if (finalIndicatorText) { + const match = finalIndicatorText.match(/(\d+) - (\d+)/); + if (match) { + const totalDocuments = parseInt(match[2]); + expect(totalDocuments).toBe(405); + expect(totalPagesLoaded).toBe(5); + } else { + throw new Error(`Invalid results indicator format: ${finalIndicatorText}`); + } + } else { + expect(totalPagesLoaded).toBe(5); + } + }); +}); \ No newline at end of file From 865e9c906b585b2d693c383500040b26f7d35d7f Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Thu, 8 Jan 2026 13:16:22 -0800 Subject: [PATCH 4/7] Run npm format --- test/mongo/pagination.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/mongo/pagination.spec.ts b/test/mongo/pagination.spec.ts index b7d0d997c..548d49184 100644 --- a/test/mongo/pagination.spec.ts +++ b/test/mongo/pagination.spec.ts @@ -48,7 +48,7 @@ test.describe("Test Mongo Pagination", () => { // Get initial results const resultText = await queryTab.resultsEditor.text(); - if (!resultText || resultText.trim() === '' || resultText.trim() === '[]') { + if (!resultText || resultText.trim() === "" || resultText.trim() === "[]") { throw new Error("Query returned no results - the collection appears to be empty"); } @@ -80,7 +80,9 @@ test.describe("Test Mongo Pagination", () => { } const beforeClickText = await queryTab.resultsEditor.text(); - const beforeClickHash = Buffer.from(beforeClickText || "").toString('base64').substring(0, 50); + const beforeClickHash = Buffer.from(beforeClickText || "") + .toString("base64") + .substring(0, 50); await loadMoreButton.click(); @@ -90,7 +92,9 @@ test.describe("Test Mongo Pagination", () => { await page.waitForTimeout(2000); const currentEditorText = await queryTab.resultsEditor.text(); - const currentHash = Buffer.from(currentEditorText || "").toString('base64').substring(0, 50); + const currentHash = Buffer.from(currentEditorText || "") + .toString("base64") + .substring(0, 50); if (currentHash !== beforeClickHash) { editorContentChanged = true; @@ -109,7 +113,7 @@ test.describe("Test Mongo Pagination", () => { } // Final verification - const finalIndicator = queryTab.resultsView.locator('text=/\\d+ - \\d+/'); + const finalIndicator = queryTab.resultsView.locator("text=/\\d+ - \\d+/"); const finalIndicatorText = await finalIndicator.textContent(); if (finalIndicatorText) { @@ -125,4 +129,4 @@ test.describe("Test Mongo Pagination", () => { expect(totalPagesLoaded).toBe(5); } }); -}); \ No newline at end of file +}); From 90c694d33c04d0990f319e90d1ae635572f7489a Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Mon, 12 Jan 2026 14:09:37 -0800 Subject: [PATCH 5/7] Fix error in tests --- test/CORSBypass.ts | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/test/CORSBypass.ts b/test/CORSBypass.ts index be52c95fa..881013c69 100644 --- a/test/CORSBypass.ts +++ b/test/CORSBypass.ts @@ -2,20 +2,54 @@ import { Page } from "@playwright/test"; export async function setupCORSBypass(page: Page) { await page.route("**/api/mongo/explorer{,/**}", async (route) => { + const request = route.request(); + const origin = request.headers()["origin"]; + + // If there's no origin, it's not a CORS request. Let it proceed without modification. + if (!origin) { + await route.continue(); + return; + } + + //// Handle preflight (OPTIONS) requests separately. + // These should not be forwarded to the target server. + if (request.method() === "OPTIONS") { + await route.fulfill({ + status: 204, // No Content + headers: { + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", + "Access-Control-Request-Headers": "*, x-ms-continuation", + "Access-Control-Max-Age": "86400", // Cache preflight response for 1 day + Vary: "Origin", + }, + }); + return; + } + + // Handle the actual GET/POST request const response = await route.fetch({ headers: { - ...route.request().headers(), + ...request.headers(), }, }); + const responseHeaders = response.headers(); + // Clean up any pre-existing CORS headers from the real response to avoid conflicts. + delete responseHeaders["access-control-allow-origin"]; + delete responseHeaders["access-control-allow-credentials"]; + await route.fulfill({ status: response.status(), headers: { - ...response.headers(), - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", + ...responseHeaders, + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Credentials": "*", + "Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token", + Vary: "Origin", }, body: await response.body(), }); From de11ece3377d0c52292ca426560ad55e60b3e0e6 Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Mon, 12 Jan 2026 17:07:23 -0800 Subject: [PATCH 6/7] Cleanup CORSByPass --- test/CORSBypass.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/test/CORSBypass.ts b/test/CORSBypass.ts index 881013c69..14d567263 100644 --- a/test/CORSBypass.ts +++ b/test/CORSBypass.ts @@ -11,7 +11,7 @@ export async function setupCORSBypass(page: Page) { return; } - //// Handle preflight (OPTIONS) requests separately. + // Handle preflight (OPTIONS) requests separately. // These should not be forwarded to the target server. if (request.method() === "OPTIONS") { await route.fulfill({ @@ -20,8 +20,7 @@ export async function setupCORSBypass(page: Page) { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", - "Access-Control-Request-Headers": "*, x-ms-continuation", - "Access-Control-Max-Age": "86400", // Cache preflight response for 1 day + "Access-Control-Allow-Headers": request.headers()["access-control-request-headers"] || "*", Vary: "Origin", }, }); @@ -35,21 +34,14 @@ export async function setupCORSBypass(page: Page) { }, }); - const responseHeaders = response.headers(); - // Clean up any pre-existing CORS headers from the real response to avoid conflicts. - delete responseHeaders["access-control-allow-origin"]; - delete responseHeaders["access-control-allow-credentials"]; - await route.fulfill({ status: response.status(), headers: { - ...responseHeaders, - "Access-Control-Allow-Origin": origin, - "Access-Control-Allow-Credentials": "true", - "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", + ...response.headers(), + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", - "Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token", - Vary: "Origin", + "Access-Control-Allow-Credentials": "*", }, body: await response.body(), }); From bd3f4f53506ab415b8ac7e19ead6dd56ecaf0db9 Mon Sep 17 00:00:00 2001 From: Sindhu Balasubramanian Date: Mon, 12 Jan 2026 17:45:04 -0800 Subject: [PATCH 7/7] Revert "Cleanup CORSByPass" This reverts commit de11ece3377d0c52292ca426560ad55e60b3e0e6. --- test/CORSBypass.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/test/CORSBypass.ts b/test/CORSBypass.ts index 14d567263..881013c69 100644 --- a/test/CORSBypass.ts +++ b/test/CORSBypass.ts @@ -11,7 +11,7 @@ export async function setupCORSBypass(page: Page) { return; } - // Handle preflight (OPTIONS) requests separately. + //// Handle preflight (OPTIONS) requests separately. // These should not be forwarded to the target server. if (request.method() === "OPTIONS") { await route.fulfill({ @@ -20,7 +20,8 @@ export async function setupCORSBypass(page: Page) { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", - "Access-Control-Allow-Headers": request.headers()["access-control-request-headers"] || "*", + "Access-Control-Request-Headers": "*, x-ms-continuation", + "Access-Control-Max-Age": "86400", // Cache preflight response for 1 day Vary: "Origin", }, }); @@ -34,14 +35,21 @@ export async function setupCORSBypass(page: Page) { }, }); + const responseHeaders = response.headers(); + // Clean up any pre-existing CORS headers from the real response to avoid conflicts. + delete responseHeaders["access-control-allow-origin"]; + delete responseHeaders["access-control-allow-credentials"]; + await route.fulfill({ status: response.status(), headers: { - ...response.headers(), - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "*", + ...responseHeaders, + "Access-Control-Allow-Origin": origin, + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS,HEAD", "Access-Control-Allow-Headers": "*", - "Access-Control-Allow-Credentials": "*", + "Access-Control-Expose-Headers": "x-ms-continuation,x-ms-request-charge,x-ms-session-token", + Vary: "Origin", }, body: await response.body(), });