@matdata/yasqe 5.5.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * OAuth 2.0 Authentication Tests
3
+ */
4
+
5
+ import { describe, it } from "mocha";
6
+ import { expect } from "chai";
7
+
8
+ describe("OAuth 2.0 Authentication", () => {
9
+ describe("Token Expiration", () => {
10
+ it("should detect expired token", () => {
11
+ // Token expired 1 hour ago
12
+ const pastTimestamp = Date.now() - 60 * 60 * 1000;
13
+ const isExpired = pastTimestamp <= Date.now();
14
+
15
+ expect(isExpired).to.be.true;
16
+ });
17
+
18
+ it("should detect valid token", () => {
19
+ // Token expires in 1 hour
20
+ const futureTimestamp = Date.now() + 60 * 60 * 1000;
21
+ const isExpired = futureTimestamp <= Date.now();
22
+
23
+ expect(isExpired).to.be.false;
24
+ });
25
+
26
+ it("should handle undefined expiry", () => {
27
+ const tokenExpiry: number | undefined = undefined;
28
+ const isExpired = !tokenExpiry || tokenExpiry <= Date.now();
29
+
30
+ expect(isExpired).to.be.true;
31
+ });
32
+
33
+ it("should add buffer time to expiration check", () => {
34
+ // Token expires in 30 seconds - should be considered expired with 60s buffer
35
+ const soonToExpire = Date.now() + 30 * 1000;
36
+ const buffer = 60 * 1000;
37
+ const isExpiredWithBuffer = soonToExpire <= Date.now() + buffer;
38
+
39
+ expect(isExpiredWithBuffer).to.be.true;
40
+ });
41
+ });
42
+
43
+ describe("Authorization Header Format", () => {
44
+ it("should create proper OAuth 2.0 Bearer header", () => {
45
+ const accessToken =
46
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
47
+ const header = `Bearer ${accessToken}`;
48
+
49
+ expect(header).to.equal(`Bearer ${accessToken}`);
50
+ expect(header).to.match(/^Bearer .+$/);
51
+ });
52
+
53
+ it("should handle various access token formats", () => {
54
+ const tokens = [
55
+ "short-token",
56
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature",
57
+ "opaque-token-12345678",
58
+ "token_with_underscores",
59
+ "token-with-dashes",
60
+ ];
61
+
62
+ tokens.forEach((token) => {
63
+ const header = `Bearer ${token}`;
64
+ expect(header).to.match(/^Bearer .+$/);
65
+ expect(header).to.equal(`Bearer ${token}`);
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("Token Storage and Retrieval", () => {
71
+ it("should store access token", () => {
72
+ const tokenData = {
73
+ accessToken: "test-access-token",
74
+ refreshToken: "test-refresh-token",
75
+ tokenExpiry: Date.now() + 3600 * 1000,
76
+ };
77
+
78
+ expect(tokenData.accessToken).to.be.a("string");
79
+ expect(tokenData.refreshToken).to.be.a("string");
80
+ expect(tokenData.tokenExpiry).to.be.a("number");
81
+ });
82
+
83
+ it("should handle missing refresh token", () => {
84
+ const tokenData = {
85
+ accessToken: "test-access-token",
86
+ refreshToken: undefined as string | undefined,
87
+ tokenExpiry: Date.now() + 3600 * 1000,
88
+ };
89
+
90
+ expect(tokenData.accessToken).to.be.a("string");
91
+ expect(tokenData.refreshToken).to.be.undefined;
92
+ });
93
+ });
94
+
95
+ describe("Token Expiry Calculation", () => {
96
+ it("should calculate correct expiry timestamp", () => {
97
+ const expiresIn = 3600; // 1 hour in seconds
98
+ const now = Date.now();
99
+ const expectedExpiry = now + expiresIn * 1000;
100
+ const calculatedExpiry = now + expiresIn * 1000;
101
+
102
+ // Allow 1 second tolerance for test execution time
103
+ expect(calculatedExpiry).to.be.closeTo(expectedExpiry, 1000);
104
+ });
105
+
106
+ it("should handle undefined expires_in", () => {
107
+ const expiresIn: number | undefined = undefined;
108
+ const expiry = expiresIn ? Date.now() + expiresIn * 1000 : undefined;
109
+
110
+ expect(expiry).to.be.undefined;
111
+ });
112
+
113
+ it("should convert seconds to milliseconds", () => {
114
+ const expiresInSeconds = 3600;
115
+ const expiresInMs = expiresInSeconds * 1000;
116
+
117
+ expect(expiresInMs).to.equal(3600000);
118
+ });
119
+ });
120
+
121
+ describe("OAuth 2.0 Configuration Validation", () => {
122
+ it("should validate required OAuth 2.0 parameters", () => {
123
+ const config = {
124
+ clientId: "my-client-id",
125
+ authorizationEndpoint: "https://provider.com/oauth/authorize",
126
+ tokenEndpoint: "https://provider.com/oauth/token",
127
+ redirectUri: "https://myapp.com/callback",
128
+ };
129
+
130
+ expect(config.clientId).to.be.a("string").and.to.have.length.greaterThan(0);
131
+ expect(config.authorizationEndpoint).to.be.a("string").and.to.include("http");
132
+ expect(config.tokenEndpoint).to.be.a("string").and.to.include("http");
133
+ expect(config.redirectUri).to.be.a("string").and.to.include("http");
134
+ });
135
+
136
+ it("should handle optional scope parameter", () => {
137
+ const config = {
138
+ clientId: "my-client-id",
139
+ authorizationEndpoint: "https://provider.com/oauth/authorize",
140
+ tokenEndpoint: "https://provider.com/oauth/token",
141
+ redirectUri: "https://myapp.com/callback",
142
+ scope: "read write",
143
+ };
144
+
145
+ expect(config.scope).to.be.a("string");
146
+ });
147
+
148
+ it("should handle missing scope parameter", () => {
149
+ const config = {
150
+ clientId: "my-client-id",
151
+ authorizationEndpoint: "https://provider.com/oauth/authorize",
152
+ tokenEndpoint: "https://provider.com/oauth/token",
153
+ redirectUri: "https://myapp.com/callback",
154
+ scope: undefined as string | undefined,
155
+ };
156
+
157
+ expect(config.scope).to.be.undefined;
158
+ });
159
+ });
160
+
161
+ describe("PKCE Code Challenge Generation", () => {
162
+ it("should generate code verifier of correct length", () => {
163
+ const length = 128;
164
+ const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
165
+ const codeVerifier = Array.from({ length }, () => possible[Math.floor(Math.random() * possible.length)]).join("");
166
+
167
+ expect(codeVerifier).to.have.length(length);
168
+ expect(codeVerifier).to.match(/^[A-Za-z0-9\-._~]+$/);
169
+ });
170
+
171
+ it("should validate PKCE characters", () => {
172
+ const validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
173
+ const testString = "abc123-._~ABC";
174
+
175
+ // Check all characters are valid
176
+ const allValid = testString.split("").every((char) => validChars.includes(char));
177
+
178
+ expect(allValid).to.be.true;
179
+ });
180
+ });
181
+
182
+ describe("URL Parameter Encoding", () => {
183
+ it("should properly encode URL parameters", () => {
184
+ const params = {
185
+ client_id: "my-client-id",
186
+ response_type: "code",
187
+ redirect_uri: "https://myapp.com/callback",
188
+ state: "random-state-string",
189
+ code_challenge: "challenge-string",
190
+ code_challenge_method: "S256",
191
+ };
192
+
193
+ const searchParams = new URLSearchParams(params);
194
+ expect(searchParams.get("client_id")).to.equal("my-client-id");
195
+ expect(searchParams.get("response_type")).to.equal("code");
196
+ expect(searchParams.get("code_challenge_method")).to.equal("S256");
197
+ });
198
+
199
+ it("should handle optional scope in URL", () => {
200
+ const params = {
201
+ client_id: "my-client-id",
202
+ scope: "read write",
203
+ };
204
+
205
+ const searchParams = new URLSearchParams(params);
206
+ expect(searchParams.get("scope")).to.equal("read write");
207
+ });
208
+ });
209
+
210
+ describe("Token Response Parsing", () => {
211
+ it("should parse token response correctly", () => {
212
+ const tokenResponse = {
213
+ access_token: "new-access-token",
214
+ refresh_token: "new-refresh-token",
215
+ expires_in: 3600,
216
+ token_type: "Bearer",
217
+ };
218
+
219
+ expect(tokenResponse.access_token).to.be.a("string");
220
+ expect(tokenResponse.refresh_token).to.be.a("string");
221
+ expect(tokenResponse.expires_in).to.be.a("number");
222
+ expect(tokenResponse.token_type).to.equal("Bearer");
223
+ });
224
+
225
+ it("should handle response without refresh token", () => {
226
+ const tokenResponse = {
227
+ access_token: "new-access-token",
228
+ expires_in: 3600,
229
+ token_type: "Bearer",
230
+ };
231
+
232
+ expect(tokenResponse.access_token).to.be.a("string");
233
+ expect(tokenResponse).to.not.have.property("refresh_token");
234
+ });
235
+ });
236
+
237
+ describe("Authentication Priority with OAuth 2.0", () => {
238
+ it("should prioritize OAuth 2.0 over Basic auth", () => {
239
+ // Both use Authorization header, OAuth 2.0 should be checked first
240
+ const oauth2Header = "Bearer oauth2-token";
241
+ const basicHeader = "Basic " + btoa("user:pass");
242
+
243
+ expect(oauth2Header).to.not.equal(basicHeader);
244
+ expect(oauth2Header).to.include("Bearer");
245
+ expect(basicHeader).to.include("Basic");
246
+ });
247
+
248
+ it("should allow API Key to coexist with OAuth 2.0", () => {
249
+ // OAuth 2.0 uses Authorization header, API Key uses custom header
250
+ const authorizationHeader = "Authorization";
251
+ const apiKeyHeader = "X-API-Key";
252
+
253
+ expect(authorizationHeader).to.not.equal(apiKeyHeader);
254
+ // These are different headers, so they can both be set
255
+ });
256
+ });
257
+
258
+ describe("Error Handling", () => {
259
+ it("should handle undefined authentication config", () => {
260
+ const oauth2Auth = undefined;
261
+
262
+ expect(oauth2Auth).to.be.undefined;
263
+ // Implementation should handle undefined gracefully
264
+ });
265
+
266
+ it("should handle null access token", () => {
267
+ const accessToken: any = null;
268
+ const isValid = !!(accessToken && typeof accessToken === "string" && accessToken.trim().length > 0);
269
+
270
+ expect(isValid).to.be.false;
271
+ });
272
+
273
+ it("should validate empty access token", () => {
274
+ const accessToken = "";
275
+ const isValid = accessToken.trim().length > 0;
276
+
277
+ expect(isValid).to.be.false;
278
+ });
279
+
280
+ it("should validate whitespace-only access token", () => {
281
+ const accessToken = " ";
282
+ const isValid = accessToken.trim().length > 0;
283
+
284
+ expect(isValid).to.be.false;
285
+ });
286
+
287
+ it("should validate non-empty access token", () => {
288
+ const accessToken = "valid-token";
289
+ const isValid = accessToken.trim().length > 0;
290
+
291
+ expect(isValid).to.be.true;
292
+ });
293
+ });
294
+
295
+ describe("Trimming Behavior", () => {
296
+ it("should use trimmed access token in header", () => {
297
+ const originalToken = " token-with-spaces ";
298
+ const trimmedToken = originalToken.trim();
299
+ const header = `Bearer ${trimmedToken}`;
300
+
301
+ expect(header).to.equal("Bearer token-with-spaces");
302
+ expect(header).to.not.include(" ");
303
+ });
304
+
305
+ it("should verify trimmed values are used not originals", () => {
306
+ const originalConfig = {
307
+ clientId: " client-id ",
308
+ scope: " read write ",
309
+ };
310
+
311
+ const trimmedConfig = {
312
+ clientId: originalConfig.clientId.trim(),
313
+ scope: originalConfig.scope.trim(),
314
+ };
315
+
316
+ expect(trimmedConfig.clientId).to.equal("client-id");
317
+ expect(trimmedConfig.scope).to.equal("read write");
318
+ });
319
+ });
320
+ });
package/src/defaults.ts CHANGED
@@ -175,6 +175,9 @@ SELECT * WHERE {
175
175
  withCredentials: false,
176
176
  adjustQueryBeforeRequest: false,
177
177
  basicAuth: undefined,
178
+ bearerAuth: undefined,
179
+ apiKeyAuth: undefined,
180
+ oauth2Auth: undefined,
178
181
  };
179
182
  return { ...config, requestConfig };
180
183
  }