Skip to content

Commit 3b12961

Browse files
Added tests
1 parent 8a222e8 commit 3b12961

File tree

3 files changed

+112
-0
lines changed

3 files changed

+112
-0
lines changed

src/client/auth.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@ describe("OAuth Authorization", () => {
4343
});
4444
});
4545

46+
it("returns metadata when first fetch fails but second without MCP header succeeds", async () => {
47+
// First request with MCP header fails
48+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
49+
50+
// Second request without header succeeds
51+
mockFetch.mockResolvedValueOnce({
52+
ok: true,
53+
status: 200,
54+
json: async () => validMetadata,
55+
});
56+
57+
const metadata = await discoverOAuthMetadata("https://auth.example.com");
58+
expect(metadata).toEqual(validMetadata);
59+
60+
// Verify second call was made without header
61+
expect(mockFetch).toHaveBeenCalledTimes(2);
62+
const secondCallOptions = mockFetch.mock.calls[1][1];
63+
expect(secondCallOptions).toBeUndefined(); // No options means no headers
64+
});
65+
66+
it("returns undefined when all fetch attempts fail", async () => {
67+
// Both requests fail
68+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
69+
mockFetch.mockRejectedValueOnce(new Error("Network error"));
70+
71+
const metadata = await discoverOAuthMetadata("https://auth.example.com");
72+
expect(metadata).toBeUndefined();
73+
expect(mockFetch).toHaveBeenCalledTimes(2);
74+
});
75+
4676
it("returns undefined when discovery endpoint returns 404", async () => {
4777
mockFetch.mockResolvedValueOnce({
4878
ok: false,

src/server/auth/handlers/register.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,37 @@ describe('Client Registration Handler', () => {
141141

142142
expect(response.status).toBe(201);
143143
expect(response.body.client_secret).toBeUndefined();
144+
expect(response.body.client_secret_expires_at).toBeUndefined();
145+
});
146+
147+
it('sets client_secret_expires_at for public clients only', async () => {
148+
// Test for public client (token_endpoint_auth_method not 'none')
149+
const publicClientMetadata: OAuthClientMetadata = {
150+
redirect_uris: ['https://example.com/callback'],
151+
token_endpoint_auth_method: 'client_secret_basic'
152+
};
153+
154+
const publicResponse = await supertest(app)
155+
.post('/register')
156+
.send(publicClientMetadata);
157+
158+
expect(publicResponse.status).toBe(201);
159+
expect(publicResponse.body.client_secret).toBeDefined();
160+
expect(publicResponse.body.client_secret_expires_at).toBeDefined();
161+
162+
// Test for non-public client (token_endpoint_auth_method is 'none')
163+
const nonPublicClientMetadata: OAuthClientMetadata = {
164+
redirect_uris: ['https://example.com/callback'],
165+
token_endpoint_auth_method: 'none'
166+
};
167+
168+
const nonPublicResponse = await supertest(app)
169+
.post('/register')
170+
.send(nonPublicClientMetadata);
171+
172+
expect(nonPublicResponse.status).toBe(201);
173+
expect(nonPublicResponse.body.client_secret).toBeUndefined();
174+
expect(nonPublicResponse.body.client_secret_expires_at).toBeUndefined();
144175
});
145176

146177
it('sets expiry based on clientSecretExpirySeconds', async () => {

src/server/auth/middleware/bearerAuth.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,57 @@ describe("requireBearerAuth middleware", () => {
5555
expect(mockResponse.status).not.toHaveBeenCalled();
5656
expect(mockResponse.json).not.toHaveBeenCalled();
5757
});
58+
59+
it("should reject expired tokens", async () => {
60+
const expiredAuthInfo: AuthInfo = {
61+
token: "expired-token",
62+
clientId: "client-123",
63+
scopes: ["read", "write"],
64+
expiresAt: Math.floor(Date.now() / 1000) - 100, // Token expired 100 seconds ago
65+
};
66+
mockVerifyAccessToken.mockResolvedValue(expiredAuthInfo);
67+
68+
mockRequest.headers = {
69+
authorization: "Bearer expired-token",
70+
};
71+
72+
const middleware = requireBearerAuth({ provider: mockProvider });
73+
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
74+
75+
expect(mockVerifyAccessToken).toHaveBeenCalledWith("expired-token");
76+
expect(mockResponse.status).toHaveBeenCalledWith(401);
77+
expect(mockResponse.set).toHaveBeenCalledWith(
78+
"WWW-Authenticate",
79+
expect.stringContaining('Bearer error="invalid_token"')
80+
);
81+
expect(mockResponse.json).toHaveBeenCalledWith(
82+
expect.objectContaining({ error: "invalid_token", error_description: "Token has expired" })
83+
);
84+
expect(nextFunction).not.toHaveBeenCalled();
85+
});
86+
87+
it("should accept non-expired tokens", async () => {
88+
const nonExpiredAuthInfo: AuthInfo = {
89+
token: "valid-token",
90+
clientId: "client-123",
91+
scopes: ["read", "write"],
92+
expiresAt: Math.floor(Date.now() / 1000) + 3600, // Token expires in an hour
93+
};
94+
mockVerifyAccessToken.mockResolvedValue(nonExpiredAuthInfo);
95+
96+
mockRequest.headers = {
97+
authorization: "Bearer valid-token",
98+
};
99+
100+
const middleware = requireBearerAuth({ provider: mockProvider });
101+
await middleware(mockRequest as Request, mockResponse as Response, nextFunction);
102+
103+
expect(mockVerifyAccessToken).toHaveBeenCalledWith("valid-token");
104+
expect(mockRequest.auth).toEqual(nonExpiredAuthInfo);
105+
expect(nextFunction).toHaveBeenCalled();
106+
expect(mockResponse.status).not.toHaveBeenCalled();
107+
expect(mockResponse.json).not.toHaveBeenCalled();
108+
});
58109

59110
it("should require specific scopes when configured", async () => {
60111
const authInfo: AuthInfo = {

0 commit comments

Comments
 (0)