Skip to content

Commit 68a462e

Browse files
committed
reformat and add integration test
1 parent ae1d6d0 commit 68a462e

File tree

2 files changed

+163
-31
lines changed

2 files changed

+163
-31
lines changed

src/tools/atlas/connect/connectCluster.ts

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,31 +30,35 @@ export class ConnectClusterTool extends AtlasToolBase {
3030

3131
private determineReadOnlyRole(): boolean {
3232
if (this.config.readOnly) return true;
33-
33+
3434
const disabledTools = this.config.disabledTools || [];
35-
const hasWriteAccess = !disabledTools.includes("create") &&
36-
!disabledTools.includes("update") &&
37-
!disabledTools.includes("delete");
38-
const hasReadAccess = !disabledTools.includes("read") &&
39-
!disabledTools.includes("metadata");
40-
35+
const hasWriteAccess =
36+
!disabledTools.includes("create") && !disabledTools.includes("update") && !disabledTools.includes("delete");
37+
const hasReadAccess = !disabledTools.includes("read") && !disabledTools.includes("metadata");
38+
4139
return !hasWriteAccess && hasReadAccess;
4240
}
4341

4442
private isConnectedToOtherCluster(projectId: string, clusterName: string): boolean {
45-
return this.session.isConnectedToMongoDB &&
46-
(!this.session.connectedAtlasCluster ||
43+
return (
44+
this.session.isConnectedToMongoDB &&
45+
(!this.session.connectedAtlasCluster ||
4746
this.session.connectedAtlasCluster.projectId !== projectId ||
48-
this.session.connectedAtlasCluster.clusterName !== clusterName);
47+
this.session.connectedAtlasCluster.clusterName !== clusterName)
48+
);
4949
}
5050

5151
private getConnectionState(): "connected" | "connecting" | "disconnected" | "errored" {
5252
const state = this.session.connectionManager.currentConnectionState;
5353
switch (state.tag) {
54-
case "connected": return "connected";
55-
case "connecting": return "connecting";
56-
case "disconnected": return "disconnected";
57-
case "errored": return "errored";
54+
case "connected":
55+
return "connected";
56+
case "connecting":
57+
return "connecting";
58+
case "disconnected":
59+
return "disconnected";
60+
case "errored":
61+
return "errored";
5862
}
5963
}
6064

@@ -140,11 +144,7 @@ export class ConnectClusterTool extends AtlasToolBase {
140144
return { username, password, expiryDate };
141145
}
142146

143-
private buildConnectionString(
144-
clusterConnectionString: string,
145-
username: string,
146-
password: string
147-
): string {
147+
private buildConnectionString(clusterConnectionString: string, username: string, password: string): string {
148148
const cn = new URL(clusterConnectionString);
149149
cn.username = username;
150150
cn.password = password;
@@ -163,11 +163,7 @@ export class ConnectClusterTool extends AtlasToolBase {
163163
}
164164

165165
const readOnly = this.determineReadOnlyRole();
166-
const { username, password, expiryDate } = await this.createDatabaseUser(
167-
projectId,
168-
clusterName,
169-
readOnly
170-
);
166+
const { username, password, expiryDate } = await this.createDatabaseUser(projectId, clusterName, readOnly);
171167

172168
const connectedAtlasCluster = {
173169
username,
@@ -176,11 +172,7 @@ export class ConnectClusterTool extends AtlasToolBase {
176172
expiryDate,
177173
};
178174

179-
const connectionString = this.buildConnectionString(
180-
cluster.connectionString,
181-
username,
182-
password
183-
);
175+
const connectionString = this.buildConnectionString(cluster.connectionString, username, password);
184176

185177
return { connectionString, atlas: connectedAtlasCluster };
186178
}
@@ -240,9 +232,11 @@ export class ConnectClusterTool extends AtlasToolBase {
240232

241233
private async cleanupDatabaseUserOnFailure(atlas: AtlasClusterConnectionInfo): Promise<void> {
242234
const currentCluster = this.session.connectedAtlasCluster;
243-
if (currentCluster?.projectId === atlas.projectId &&
235+
if (
236+
currentCluster?.projectId === atlas.projectId &&
244237
currentCluster?.clusterName === atlas.clusterName &&
245-
currentCluster?.username) {
238+
currentCluster?.username
239+
) {
246240
try {
247241
await this.session.apiClient.deleteDatabaseUser({
248242
params: {
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2+
import { DeviceIdService } from "../../../src/helpers/deviceId.js";
3+
import { CompositeLogger } from "../../../src/common/logger.js";
4+
import nodeMachineId from "node-machine-id";
5+
6+
describe("Device ID", () => {
7+
let testLogger: CompositeLogger;
8+
9+
beforeEach(() => {
10+
testLogger = new CompositeLogger();
11+
testLogger.debug = vi.fn();
12+
});
13+
14+
afterEach(() => {
15+
if (DeviceIdService.isInitialized()) {
16+
DeviceIdService.getInstance().close();
17+
}
18+
});
19+
20+
describe("when resolving device ID", () => {
21+
it("should successfully resolve device ID in real environment", async () => {
22+
const deviceId = DeviceIdService.init(testLogger);
23+
const result = await deviceId.getDeviceId();
24+
25+
expect(result).not.toBe("unknown");
26+
expect(result).toBeTruthy();
27+
expect(typeof result).toBe("string");
28+
expect(result.length).toBeGreaterThan(0);
29+
});
30+
31+
it("should cache device ID after first resolution", async () => {
32+
// spy on machineId
33+
const machineIdSpy = vi.spyOn(nodeMachineId, "machineId");
34+
const deviceId = DeviceIdService.init(testLogger);
35+
36+
// First call
37+
const result1 = await deviceId.getDeviceId();
38+
expect(result1).not.toBe("unknown");
39+
40+
// Second call should be cached
41+
const result2 = await deviceId.getDeviceId();
42+
expect(result2).toBe(result1);
43+
// check that machineId was called only once
44+
expect(machineIdSpy).toHaveBeenCalledOnce();
45+
});
46+
47+
it("should handle concurrent device ID requests correctly", async () => {
48+
const deviceId = DeviceIdService.init(testLogger);
49+
50+
const promises = Array.from({ length: 5 }, () => deviceId.getDeviceId());
51+
52+
// All should resolve to the same value
53+
const results = await Promise.all(promises);
54+
const firstResult = results[0];
55+
expect(firstResult).not.toBe("unknown");
56+
57+
// All results should be identical
58+
results.forEach((result) => {
59+
expect(result).toBe(firstResult);
60+
});
61+
});
62+
});
63+
64+
describe("when resolving device ID fails", () => {
65+
const originalMachineId: typeof nodeMachineId.machineId = nodeMachineId.machineId;
66+
67+
beforeEach(() => {
68+
// mock the machineId function to throw an abort error
69+
nodeMachineId.machineId = vi.fn();
70+
});
71+
72+
afterEach(() => {
73+
// Restore original implementation
74+
nodeMachineId.machineId = originalMachineId;
75+
});
76+
77+
it("should handle resolution errors gracefully", async () => {
78+
// mock the machineId function to throw a resolution error
79+
nodeMachineId.machineId = vi.fn().mockImplementation(() => {
80+
return new Promise<string>((resolve, reject) => {
81+
reject(new Error("Machine ID failed"));
82+
});
83+
});
84+
const deviceId = DeviceIdService.init(testLogger);
85+
const handleDeviceIdErrorSpy = vi.spyOn(deviceId, "handleDeviceIdError" as keyof DeviceIdService);
86+
87+
const result = await deviceId.getDeviceId();
88+
89+
expect(result).toBe("unknown");
90+
expect(handleDeviceIdErrorSpy).toHaveBeenCalledWith(
91+
"resolutionError",
92+
expect.stringContaining("Machine ID failed")
93+
);
94+
});
95+
96+
it("should handle abort signal scenarios gracefully", async () => {
97+
// slow down the machineId function to give time to send abort signal
98+
nodeMachineId.machineId = vi.fn().mockImplementation(() => {
99+
return new Promise<string>((resolve) => {
100+
setTimeout(() => resolve("delayed-id"), 1000);
101+
});
102+
});
103+
104+
const deviceId = DeviceIdService.init(testLogger);
105+
const handleDeviceIdErrorSpy = vi.spyOn(deviceId, "handleDeviceIdError" as keyof DeviceIdService);
106+
107+
deviceId.close();
108+
109+
// expect the deviceId service to throw an error
110+
await expect(deviceId.getDeviceId()).rejects.toThrow(Error);
111+
// test that the private function handleDeviceIdError was called with reason "abort"
112+
expect(handleDeviceIdErrorSpy).toHaveBeenCalledWith(
113+
"abort",
114+
expect.stringContaining("Aborted by abort signal")
115+
);
116+
117+
// check that the deviceId service is not initialized anymore
118+
expect(() => DeviceIdService.getInstance()).toThrow(Error);
119+
});
120+
121+
it("should handle timeout scenarios gracefully", async () => {
122+
nodeMachineId.machineId = vi.fn().mockImplementation(() => {
123+
return new Promise<string>((resolve) => {
124+
setTimeout(() => resolve("delayed-id"), 200);
125+
});
126+
});
127+
128+
// override the timeout to 100ms
129+
const deviceId = DeviceIdService.init(testLogger, 100);
130+
const handleDeviceIdErrorSpy = vi.spyOn(deviceId, "handleDeviceIdError" as keyof DeviceIdService);
131+
132+
const result = await deviceId.getDeviceId();
133+
134+
expect(result).toBe("unknown");
135+
expect(handleDeviceIdErrorSpy).toHaveBeenCalledWith("timeout", expect.stringContaining("Timeout"));
136+
}, 5000);
137+
});
138+
});

0 commit comments

Comments
 (0)