-
Notifications
You must be signed in to change notification settings - Fork 30
fix: validate creds #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
fix: validate creds #222
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
import { jest } from "@jest/globals"; | ||
import { ApiClient } from "../../src/common/atlas/apiClient.js"; | ||
import { CommonProperties, TelemetryEvent, TelemetryResult } from "../../src/telemetry/types.js"; | ||
|
||
describe("ApiClient", () => { | ||
let apiClient: ApiClient; | ||
|
||
const mockEvents: TelemetryEvent<CommonProperties>[] = [ | ||
{ | ||
timestamp: new Date().toISOString(), | ||
source: "mdbmcp", | ||
properties: { | ||
mcp_client_version: "1.0.0", | ||
mcp_client_name: "test-client", | ||
mcp_server_version: "1.0.0", | ||
mcp_server_name: "test-server", | ||
platform: "test-platform", | ||
arch: "test-arch", | ||
os_type: "test-os", | ||
component: "test-component", | ||
duration_ms: 100, | ||
result: "success" as TelemetryResult, | ||
category: "test-category", | ||
}, | ||
}, | ||
]; | ||
|
||
beforeEach(() => { | ||
apiClient = new ApiClient({ | ||
baseUrl: "https://api.test.com", | ||
credentials: { | ||
clientId: "test-client-id", | ||
clientSecret: "test-client-secret", | ||
}, | ||
userAgent: "test-user-agent", | ||
}); | ||
|
||
// @ts-expect-error accessing private property for testing | ||
apiClient.getAccessToken = jest.fn().mockResolvedValue("mockToken"); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
describe("constructor", () => { | ||
it("should create a client with the correct configuration", () => { | ||
expect(apiClient).toBeDefined(); | ||
expect(apiClient.hasCredentials()).toBeDefined(); | ||
}); | ||
}); | ||
|
||
describe("listProjects", () => { | ||
it("should return a list of projects", async () => { | ||
const mockProjects = { | ||
results: [ | ||
{ id: "1", name: "Project 1" }, | ||
{ id: "2", name: "Project 2" }, | ||
], | ||
totalCount: 2, | ||
}; | ||
|
||
const mockGet = jest.fn().mockImplementation(() => ({ | ||
data: mockProjects, | ||
error: null, | ||
response: new Response(), | ||
})); | ||
|
||
// @ts-expect-error accessing private property for testing | ||
apiClient.client.GET = mockGet; | ||
|
||
const result = await apiClient.listProjects(); | ||
|
||
expect(mockGet).toHaveBeenCalledWith("/api/atlas/v2/groups", undefined); | ||
expect(result).toEqual(mockProjects); | ||
}); | ||
|
||
it("should throw an error when the API call fails", async () => { | ||
const mockError = { | ||
reason: "Test error", | ||
detail: "Something went wrong", | ||
}; | ||
|
||
const mockGet = jest.fn().mockImplementation(() => ({ | ||
data: null, | ||
error: mockError, | ||
response: new Response(), | ||
})); | ||
|
||
// @ts-expect-error accessing private property for testing | ||
apiClient.client.GET = mockGet; | ||
|
||
await expect(apiClient.listProjects()).rejects.toThrow(); | ||
}); | ||
}); | ||
|
||
describe("sendEvents", () => { | ||
it("should send events to authenticated endpoint when token is available", async () => { | ||
const mockFetch = jest.spyOn(global, "fetch"); | ||
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); | ||
|
||
await apiClient.sendEvents(mockEvents); | ||
|
||
const url = new URL("api/private/v1.0/telemetry/events", "https://api.test.com"); | ||
expect(mockFetch).toHaveBeenCalledWith(url, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Authorization: "Bearer mockToken", | ||
|
||
Accept: "application/json", | ||
"User-Agent": "test-user-agent", | ||
}, | ||
body: JSON.stringify(mockEvents), | ||
}); | ||
}); | ||
|
||
it("should fall back to unauthenticated endpoint when token is not available", async () => { | ||
const mockFetch = jest.spyOn(global, "fetch"); | ||
mockFetch.mockResolvedValueOnce(new Response(null, { status: 200 })); | ||
|
||
// @ts-expect-error accessing private property for testing | ||
apiClient.getAccessToken = jest.fn().mockResolvedValue(undefined); | ||
|
||
await apiClient.sendEvents(mockEvents); | ||
|
||
const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com"); | ||
expect(mockFetch).toHaveBeenCalledWith(url, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Accept: "application/json", | ||
"User-Agent": "test-user-agent", | ||
}, | ||
body: JSON.stringify(mockEvents), | ||
}); | ||
}); | ||
|
||
it("should fall back to unauthenticated endpoint on 401 error", async () => { | ||
const mockFetch = jest.spyOn(global, "fetch"); | ||
mockFetch | ||
.mockResolvedValueOnce(new Response(null, { status: 401 })) | ||
.mockResolvedValueOnce(new Response(null, { status: 200 })); | ||
|
||
await apiClient.sendEvents(mockEvents); | ||
|
||
const url = new URL("api/private/unauth/telemetry/events", "https://api.test.com"); | ||
expect(mockFetch).toHaveBeenCalledTimes(2); | ||
expect(mockFetch).toHaveBeenLastCalledWith(url, { | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/json", | ||
Accept: "application/json", | ||
"User-Agent": "test-user-agent", | ||
}, | ||
body: JSON.stringify(mockEvents), | ||
}); | ||
}); | ||
|
||
it("should throw error when both authenticated and unauthenticated requests fail", async () => { | ||
const mockFetch = jest.spyOn(global, "fetch"); | ||
mockFetch | ||
.mockResolvedValueOnce(new Response(null, { status: 401 })) | ||
.mockResolvedValueOnce(new Response(null, { status: 500 })); | ||
|
||
const mockToken = "test-token"; | ||
// @ts-expect-error accessing private property for testing | ||
apiClient.getAccessToken = jest.fn().mockResolvedValue(mockToken); | ||
|
||
await expect(apiClient.sendEvents(mockEvents)).rejects.toThrow(); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we ever get here? Doesn't hurt to have it just in case, but also wondering if I'm not seeing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if someone sets connection string + invalid api keys, we let them start the server
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if fetch errors, we'd get here right?actually not sureThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'd get here if we get a 401, if any other error, we do nothing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, my point was that we need to get a 401 wrapped in a
ApiClientError
, whichfetch
shouldn't be doing, should it? The only place we can throwApiClientError
is on line 146, but that explicitly avoids throwing for 401s.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
got it! i think you're right, let me try to fix it