Merge branch 'master' of https://github.com/Azure/cosmos-explorer into users/aisayas/mongo-cassandra-connectionstring-login-e2etests

This commit is contained in:
Asier Isayas
2026-09-16 13:20:35 -07:00
6 changed files with 120 additions and 26 deletions
+78 -17
View File
@@ -1,13 +1,39 @@
jest.mock("../../Utils/arm/request"); jest.mock("../../Utils/arm/request");
jest.mock("../CosmosClient"); jest.mock("../CosmosClient");
jest.mock("../Logger");
jest.mock("../ErrorHandlingUtils", () => ({ handleError: jest.fn() }));
jest.mock("../../Utils/NotificationConsoleUtils");
import * as Logger from "Common/Logger";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { updateUserContext } from "../../UserContext"; import { updateUserContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { armRequest } from "../../Utils/arm/request"; import { armRequest } from "../../Utils/arm/request";
import { client } from "../CosmosClient"; import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
import { readCollections } from "./readCollections"; import { readCollections } from "./readCollections";
describe("readCollections", () => { describe("readCollections", () => {
const clearMessage = jest.fn();
const fetchAll = jest.fn();
const readAll = jest.fn(() => ({ fetchAll }));
const database = jest.fn(() => ({ containers: { readAll } }));
const diagnostics = {
clientSideRequestStatistics: {
requestDurationInMs: 123,
locationEndpointsContacted: ["https://test.documents.azure.com"],
retryDiagnostics: { failedAttempts: [{ statusCode: 429 }] },
},
diagnosticNode: { data: { responsePayload: "not-for-logging" } },
};
beforeEach(() => {
jest.clearAllMocks();
(logConsoleProgress as jest.Mock).mockReturnValue(clearMessage);
(client as jest.Mock).mockReturnValue({ database });
updateUserContext({ authType: AuthType.MasterKey });
});
beforeAll(() => { beforeAll(() => {
updateUserContext({ updateUserContext({
databaseAccount: { databaseAccount: {
@@ -23,26 +49,61 @@ describe("readCollections", () => {
}); });
await readCollections("database"); await readCollections("database");
expect(armRequest).toHaveBeenCalled(); expect(armRequest).toHaveBeenCalled();
expect(client).not.toHaveBeenCalled();
expect(clearMessage).toHaveBeenCalledTimes(1);
}); });
it("should call SDK if not logged in with non-AAD method", async () => { it("should log SDK request statistics and return collections for non-AAD authentication", async () => {
updateUserContext({ const resources = [{ id: "container" }];
authType: AuthType.MasterKey, fetchAll.mockResolvedValue({ resources, diagnostics });
await expect(readCollections("database")).resolves.toBe(resources);
expect(database).toHaveBeenCalledWith("database");
expect(readAll).toHaveBeenCalledWith();
expect(fetchAll).toHaveBeenCalledTimes(1);
expect(Logger.logInfo).toHaveBeenLastCalledWith(
expect.stringContaining(`diagnostics=${JSON.stringify(diagnostics.clientSideRequestStatistics)}`),
"readCollections",
);
expect(JSON.stringify(jest.mocked(Logger.logInfo).mock.calls)).not.toContain("not-for-logging");
expect(clearMessage).toHaveBeenCalledTimes(1);
}); });
(client as jest.Mock).mockReturnValue({
database: () => { it("should log SDK failure statistics and rethrow the original error", async () => {
return { const error = Object.assign(new Error("fetchAll failed"), { diagnostics });
containers: { fetchAll.mockRejectedValue(error);
readAll: () => {
return { await expect(readCollections("database")).rejects.toBe(error);
fetchAll: (): unknown => [],
}; expect(Logger.logError).toHaveBeenCalledWith(
}, `readCollections: fetchAll failed for database database, diagnostics=${JSON.stringify(
}, diagnostics.clientSideRequestStatistics,
}; )}`,
}, "readCollections",
);
expect(handleError).toHaveBeenCalledWith(
error,
"ReadCollections",
"Error while querying containers for database database",
);
expect(clearMessage).toHaveBeenCalledTimes(1);
}); });
await readCollections("database");
expect(client).toHaveBeenCalled(); it("should preserve error handling when diagnostics are unavailable", async () => {
const error = new Error("client initialization failed");
(client as jest.Mock).mockImplementationOnce(() => {
throw error;
});
await expect(readCollections("database")).rejects.toBe(error);
expect(Logger.logError).not.toHaveBeenCalled();
expect(handleError).toHaveBeenCalledWith(
error,
"ReadCollections",
"Error while querying containers for database database",
);
expect(clearMessage).toHaveBeenCalledTimes(1);
}); });
}); });
+12 -1
View File
@@ -78,7 +78,9 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll(); const sdkResponse = await client().database(databaseId).containers.readAll().fetchAll();
Logger.logInfo( Logger.logInfo(
`readCollections: fetchAll completed for database ${databaseId}, count=${sdkResponse.resources `readCollections: fetchAll completed for database ${databaseId}, count=${sdkResponse.resources
?.length}, durationMs=${Date.now() - fetchAllStart}`, ?.length}, durationMs=${Date.now() - fetchAllStart}, diagnostics=${JSON.stringify(
sdkResponse.diagnostics?.clientSideRequestStatistics,
)}`,
"readCollections", "readCollections",
); );
traceSuccess( traceSuccess(
@@ -88,6 +90,15 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
); );
return sdkResponse.resources as DataModels.Collection[]; return sdkResponse.resources as DataModels.Collection[];
} catch (error) { } catch (error) {
const diagnostics = error instanceof Error && "diagnostics" in error ? error.diagnostics : undefined;
if (diagnostics && typeof diagnostics === "object" && "clientSideRequestStatistics" in diagnostics) {
Logger.logError(
`readCollections: fetchAll failed for database ${databaseId}, diagnostics=${JSON.stringify(
diagnostics.clientSideRequestStatistics,
)}`,
"readCollections",
);
}
traceFailure(Action.ReadCollections, { databaseId, error: error?.message }, startKey); traceFailure(Action.ReadCollections, { databaseId, error: error?.message }, startKey);
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`); handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error; throw error;
+5
View File
@@ -39,6 +39,7 @@ export interface DatabaseAccountBackupPolicy {
} }
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
apiProperties?: DatabaseAccountApiProperties;
documentEndpoint?: string; documentEndpoint?: string;
disableLocalAuth?: boolean; disableLocalAuth?: boolean;
tableEndpoint?: string; tableEndpoint?: string;
@@ -68,6 +69,10 @@ export interface DatabaseAccountExtendedProperties {
enableAllVersionsAndDeletesChangeFeed?: boolean; enableAllVersionsAndDeletesChangeFeed?: boolean;
} }
export interface DatabaseAccountApiProperties {
serverVersion?: string;
}
export interface DatabaseAccountResponseLocation { export interface DatabaseAccountResponseLocation {
documentEndpoint: string; documentEndpoint: string;
failoverPriority: number; failoverPriority: number;
@@ -26,6 +26,9 @@ export const EXIT_COMMAND_MONGO = ` printf "\\033[1;31mSession ended. Please clo
*/ */
export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`; export const DISABLE_TELEMETRY_COMMAND = `mongosh --nodb --quiet --eval 'disableTelemetry()'`;
const MONGOSH_PACKAGE_VERSION = "2.5.6";
const MONGOSH_36_PACKAGE_VERSION = "1.10.6";
/** /**
* Abstract class that defines the interface for shell-specific handlers * Abstract class that defines the interface for shell-specific handlers
* in the CloudShell terminal implementation. Each supported shell type * in the CloudShell terminal implementation. Each supported shell type
@@ -96,14 +99,14 @@ export abstract class AbstractShellHandler {
* Each command runs conditionally only if mongosh * Each command runs conditionally only if mongosh
* is not already present in the environment. * is not already present in the environment.
*/ */
protected mongoShellSetupCommands(): string[] { protected mongoShellSetupCommands(serverVersion?: string): string[] {
const PACKAGE_VERSION: string = "2.5.6"; const packageVersion = serverVersion === "3.6" ? MONGOSH_36_PACKAGE_VERSION : MONGOSH_PACKAGE_VERSION;
return [ return [
"if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi", "if ! command -v mongosh &> /dev/null; then echo '⚠️ mongosh not found. Installing...'; fi",
`if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`, `if ! command -v mongosh &> /dev/null; then curl -LO https://downloads.mongodb.com/compass/mongosh-${packageVersion}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`, `if ! command -v mongosh &> /dev/null; then tar -xvzf mongosh-${packageVersion}-linux-x64.tgz; fi`,
`if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${PACKAGE_VERSION}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`, `if ! command -v mongosh &> /dev/null; then mkdir -p ~/mongosh/bin && mv mongosh-${packageVersion}-linux-x64/bin/mongosh ~/mongosh/bin/ && chmod +x ~/mongosh/bin/mongosh; fi`,
`if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${PACKAGE_VERSION}-linux-x64 mongosh-${PACKAGE_VERSION}-linux-x64.tgz; fi`, `if ! command -v mongosh &> /dev/null; then rm -rf mongosh-${packageVersion}-linux-x64 mongosh-${packageVersion}-linux-x64.tgz; fi`,
"if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi", "if ! command -v mongosh &> /dev/null; then echo 'export PATH=$HOME/mongosh/bin:$PATH' >> ~/.bashrc; fi",
"if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi", "if ! command -v mongosh &> /dev/null; then source ~/.bashrc; fi",
]; ];
@@ -5,6 +5,9 @@ import { MongoShellHandler } from "./MongoShellHandler";
// Define interfaces for type safety // Define interfaces for type safety
interface DatabaseAccountProperties { interface DatabaseAccountProperties {
mongoEndpoint?: string; mongoEndpoint?: string;
apiProperties?: {
serverVersion?: string;
};
} }
interface DatabaseAccount { interface DatabaseAccount {
@@ -80,6 +83,17 @@ describe("MongoShellHandler", () => {
expect(commands.length).toBe(7); expect(commands.length).toBe(7);
expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz"); expect(commands[1]).toContain("mongosh-2.5.6-linux-x64.tgz");
}); });
it("should download a MongoDB 3.6-compatible package for 3.6 accounts", () => {
const properties = (userContext as UserContextType).databaseAccount.properties;
const originalApiProperties = properties.apiProperties;
properties.apiProperties = { serverVersion: "3.6" };
const commands = mongoShellHandler.getSetUpCommands();
expect(commands[1]).toContain("mongosh-1.10.6-linux-x64.tgz");
properties.apiProperties = originalApiProperties;
});
}); });
describe("getConnectionCommand", () => { describe("getConnectionCommand", () => {
@@ -29,7 +29,7 @@ export class MongoShellHandler extends AbstractShellHandler {
} }
public getSetUpCommands(): string[] { public getSetUpCommands(): string[] {
return this.mongoShellSetupCommands(); return this.mongoShellSetupCommands(userContext.databaseAccount?.properties.apiProperties?.serverVersion);
} }
public getConnectionCommand(): string { public getConnectionCommand(): string {