Skip to content

Commit 476c111

Browse files
committed
Add Proxy Provider test
1 parent 996822f commit 476c111

File tree

2 files changed

+287
-2
lines changed

2 files changed

+287
-2
lines changed

src/server/auth/proxyProvider.test.ts

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import { Response } from "express";
2+
import { ProxyOAuthServerProvider, ProxyOptions } from "./proxyProvider.js";
3+
import { AuthInfo } from "./types.js";
4+
import { OAuthClientInformationFull, OAuthTokens } from "../../shared/auth.js";
5+
import { ServerError } from "./errors.js";
6+
7+
describe("Proxy OAuth Server Provider", () => {
8+
// Mock client data
9+
const validClient: OAuthClientInformationFull = {
10+
client_id: "test-client",
11+
client_secret: "test-secret",
12+
redirect_uris: ["https://example.com/callback"],
13+
};
14+
15+
// Mock response object
16+
const mockResponse = {
17+
redirect: jest.fn(),
18+
} as unknown as Response;
19+
20+
// Base provider options
21+
const baseOptions: ProxyOptions = {
22+
endpoints: {
23+
authorizationUrl: "https://auth.example.com/authorize",
24+
tokenUrl: "https://auth.example.com/token",
25+
revocationUrl: "https://auth.example.com/revoke",
26+
registrationUrl: "https://auth.example.com/register",
27+
},
28+
verifyToken: jest.fn().mockImplementation(async (token: string) => {
29+
if (token === "valid-token") {
30+
return {
31+
token,
32+
clientId: "test-client",
33+
scopes: ["read", "write"],
34+
expiresAt: Date.now() / 1000 + 3600,
35+
} as AuthInfo;
36+
}
37+
throw new Error("Invalid token");
38+
}),
39+
getClient: jest.fn().mockImplementation(async (clientId: string) => {
40+
if (clientId === "test-client") {
41+
return validClient;
42+
}
43+
return undefined;
44+
}),
45+
};
46+
47+
let provider: ProxyOAuthServerProvider;
48+
let originalFetch: typeof global.fetch;
49+
50+
beforeEach(() => {
51+
provider = new ProxyOAuthServerProvider(baseOptions);
52+
originalFetch = global.fetch;
53+
global.fetch = jest.fn();
54+
});
55+
56+
afterEach(() => {
57+
global.fetch = originalFetch;
58+
jest.clearAllMocks();
59+
});
60+
61+
describe("authorization", () => {
62+
it("redirects to authorization endpoint with correct parameters", async () => {
63+
await provider.authorize(
64+
validClient,
65+
{
66+
redirectUri: "https://example.com/callback",
67+
codeChallenge: "test-challenge",
68+
state: "test-state",
69+
scopes: ["read", "write"],
70+
},
71+
mockResponse
72+
);
73+
74+
const expectedUrl = new URL("https://auth.example.com/authorize");
75+
expectedUrl.searchParams.set("client_id", "test-client");
76+
expectedUrl.searchParams.set("response_type", "code");
77+
expectedUrl.searchParams.set("redirect_uri", "https://example.com/callback");
78+
expectedUrl.searchParams.set("code_challenge", "test-challenge");
79+
expectedUrl.searchParams.set("code_challenge_method", "S256");
80+
expectedUrl.searchParams.set("state", "test-state");
81+
expectedUrl.searchParams.set("scope", "read write");
82+
83+
expect(mockResponse.redirect).toHaveBeenCalledWith(expectedUrl.toString());
84+
});
85+
86+
it("throws error when authorization endpoint is not configured", async () => {
87+
const providerWithoutAuth = new ProxyOAuthServerProvider({
88+
...baseOptions,
89+
endpoints: { ...baseOptions.endpoints, authorizationUrl: undefined },
90+
});
91+
92+
await expect(
93+
providerWithoutAuth.authorize(validClient, {
94+
redirectUri: "https://example.com/callback",
95+
codeChallenge: "test-challenge",
96+
}, mockResponse)
97+
).rejects.toThrow("No authorization endpoint configured");
98+
});
99+
});
100+
101+
describe("token exchange", () => {
102+
const mockTokenResponse: OAuthTokens = {
103+
access_token: "new-access-token",
104+
token_type: "Bearer",
105+
expires_in: 3600,
106+
refresh_token: "new-refresh-token",
107+
};
108+
109+
beforeEach(() => {
110+
(global.fetch as jest.Mock).mockImplementation(() =>
111+
Promise.resolve({
112+
ok: true,
113+
json: () => Promise.resolve(mockTokenResponse),
114+
})
115+
);
116+
});
117+
118+
it("exchanges authorization code for tokens", async () => {
119+
const tokens = await provider.exchangeAuthorizationCode(
120+
validClient,
121+
"test-code",
122+
"test-verifier"
123+
);
124+
125+
expect(global.fetch).toHaveBeenCalledWith(
126+
"https://auth.example.com/token",
127+
expect.objectContaining({
128+
method: "POST",
129+
headers: {
130+
"Content-Type": "application/x-www-form-urlencoded",
131+
},
132+
body: expect.stringContaining("grant_type=authorization_code")
133+
})
134+
);
135+
expect(tokens).toEqual(mockTokenResponse);
136+
});
137+
138+
it("exchanges refresh token for new tokens", async () => {
139+
const tokens = await provider.exchangeRefreshToken(
140+
validClient,
141+
"test-refresh-token",
142+
["read", "write"]
143+
);
144+
145+
expect(global.fetch).toHaveBeenCalledWith(
146+
"https://auth.example.com/token",
147+
expect.objectContaining({
148+
method: "POST",
149+
headers: {
150+
"Content-Type": "application/x-www-form-urlencoded",
151+
},
152+
body: expect.stringContaining("grant_type=refresh_token")
153+
})
154+
);
155+
expect(tokens).toEqual(mockTokenResponse);
156+
});
157+
158+
it("throws error when token endpoint is not configured", async () => {
159+
const providerWithoutToken = new ProxyOAuthServerProvider({
160+
...baseOptions,
161+
endpoints: { ...baseOptions.endpoints, tokenUrl: undefined },
162+
});
163+
164+
await expect(
165+
providerWithoutToken.exchangeAuthorizationCode(validClient, "test-code")
166+
).rejects.toThrow("No token endpoint configured");
167+
});
168+
169+
it("handles token exchange failure", async () => {
170+
(global.fetch as jest.Mock).mockImplementation(() =>
171+
Promise.resolve({
172+
ok: false,
173+
status: 400,
174+
})
175+
);
176+
177+
await expect(
178+
provider.exchangeAuthorizationCode(validClient, "invalid-code")
179+
).rejects.toThrow(ServerError);
180+
});
181+
});
182+
183+
describe("client registration", () => {
184+
it("registers new client", async () => {
185+
const newClient: OAuthClientInformationFull = {
186+
client_id: "new-client",
187+
redirect_uris: ["https://new-client.com/callback"],
188+
};
189+
190+
(global.fetch as jest.Mock).mockImplementation(() =>
191+
Promise.resolve({
192+
ok: true,
193+
json: () => Promise.resolve(newClient),
194+
})
195+
);
196+
197+
const result = await provider.clientsStore.registerClient!(newClient);
198+
199+
expect(global.fetch).toHaveBeenCalledWith(
200+
"https://auth.example.com/register",
201+
expect.objectContaining({
202+
method: "POST",
203+
headers: {
204+
"Content-Type": "application/json",
205+
},
206+
body: JSON.stringify(newClient),
207+
})
208+
);
209+
expect(result).toEqual(newClient);
210+
});
211+
212+
it("handles registration failure", async () => {
213+
(global.fetch as jest.Mock).mockImplementation(() =>
214+
Promise.resolve({
215+
ok: false,
216+
status: 400,
217+
})
218+
);
219+
220+
const newClient: OAuthClientInformationFull = {
221+
client_id: "new-client",
222+
redirect_uris: ["https://new-client.com/callback"],
223+
};
224+
225+
await expect(
226+
provider.clientsStore.registerClient!(newClient)
227+
).rejects.toThrow(ServerError);
228+
});
229+
});
230+
231+
describe("token revocation", () => {
232+
it("revokes token", async () => {
233+
(global.fetch as jest.Mock).mockImplementation(() =>
234+
Promise.resolve({
235+
ok: true,
236+
})
237+
);
238+
239+
await provider.revokeToken!(validClient, {
240+
token: "token-to-revoke",
241+
token_type_hint: "access_token",
242+
});
243+
244+
expect(global.fetch).toHaveBeenCalledWith(
245+
"https://auth.example.com/revoke",
246+
expect.objectContaining({
247+
method: "POST",
248+
headers: {
249+
"Content-Type": "application/x-www-form-urlencoded",
250+
},
251+
body: expect.stringContaining("token=token-to-revoke"),
252+
})
253+
);
254+
});
255+
256+
it("handles revocation failure", async () => {
257+
(global.fetch as jest.Mock).mockImplementation(() =>
258+
Promise.resolve({
259+
ok: false,
260+
status: 400,
261+
})
262+
);
263+
264+
await expect(
265+
provider.revokeToken!(validClient, {
266+
token: "invalid-token",
267+
})
268+
).rejects.toThrow(ServerError);
269+
});
270+
});
271+
272+
describe("token verification", () => {
273+
it("verifies valid token", async () => {
274+
const authInfo = await provider.verifyAccessToken("valid-token");
275+
expect(authInfo.token).toBe("valid-token");
276+
expect(baseOptions.verifyToken).toHaveBeenCalledWith("valid-token");
277+
});
278+
279+
it("rejects invalid token", async () => {
280+
await expect(
281+
provider.verifyAccessToken("invalid-token")
282+
).rejects.toThrow("Invalid token");
283+
});
284+
});
285+
});

src/server/auth/proxyProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
7878
headers: {
7979
"Content-Type": "application/x-www-form-urlencoded",
8080
},
81-
body: params,
81+
body: params.toString(),
8282
});
8383

8484
if (!response.ok) {
@@ -222,7 +222,7 @@ export class ProxyOAuthServerProvider implements OAuthServerProvider {
222222
headers: {
223223
"Content-Type": "application/x-www-form-urlencoded",
224224
},
225-
body: params,
225+
body: params.toString(),
226226
});
227227

228228
if (!response.ok) {

0 commit comments

Comments
 (0)