@matdata/yasqe 5.5.0 → 5.6.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@matdata/yasqe",
3
3
  "description": "Yet Another SPARQL Query Editor",
4
- "version": "5.5.0",
4
+ "version": "5.6.0",
5
5
  "main": "build/yasqe.min.js",
6
6
  "types": "build/ts/src/index.d.ts",
7
7
  "license": "MIT",
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Authentication Tests
3
+ * Tests for Basic Auth, Bearer Token, and API Key authentication
4
+ */
5
+
6
+ import { describe, it } from "mocha";
7
+ import { expect } from "chai";
8
+
9
+ describe("Authentication", () => {
10
+ describe("Basic Authentication", () => {
11
+ describe("Base64 Encoding", () => {
12
+ it("should encode credentials correctly", () => {
13
+ const username = "testuser";
14
+ const password = "testpass";
15
+ const credentials = `${username}:${password}`;
16
+ const encoded = btoa(credentials);
17
+ const expected = "dGVzdHVzZXI6dGVzdHBhc3M=";
18
+
19
+ expect(encoded).to.equal(expected);
20
+ });
21
+
22
+ it("should handle special characters", () => {
23
+ const username = "user@example.com";
24
+ const password = "p@ss:word!";
25
+ const credentials = `${username}:${password}`;
26
+ const encoded = btoa(credentials);
27
+
28
+ // Verify it can be decoded back
29
+ const decoded = atob(encoded);
30
+ expect(decoded).to.equal(credentials);
31
+ });
32
+ });
33
+
34
+ describe("Authorization Header Format", () => {
35
+ it("should create proper Basic auth header", () => {
36
+ const username = "admin";
37
+ const password = "secret";
38
+ const credentials = `${username}:${password}`;
39
+ const encoded = btoa(credentials);
40
+ const header = `Basic ${encoded}`;
41
+
42
+ expect(header).to.equal("Basic YWRtaW46c2VjcmV0");
43
+ expect(header).to.match(/^Basic [A-Za-z0-9+/=]+$/);
44
+ });
45
+ });
46
+
47
+ describe("Empty Credentials", () => {
48
+ it("should handle empty username", () => {
49
+ const username = "";
50
+ const password = "password";
51
+ const credentials = `${username}:${password}`;
52
+ const encoded = btoa(credentials);
53
+
54
+ expect(encoded).to.be.a("string");
55
+ expect(encoded.length).to.be.greaterThan(0);
56
+ });
57
+
58
+ it("should handle empty password", () => {
59
+ const username = "user";
60
+ const password = "";
61
+ const credentials = `${username}:${password}`;
62
+ const encoded = btoa(credentials);
63
+
64
+ expect(encoded).to.be.a("string");
65
+ expect(encoded.length).to.be.greaterThan(0);
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("Bearer Token Authentication", () => {
71
+ describe("Authorization Header Format", () => {
72
+ it("should create proper Bearer auth header", () => {
73
+ const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
74
+ const header = `Bearer ${token}`;
75
+
76
+ expect(header).to.equal(`Bearer ${token}`);
77
+ expect(header).to.match(/^Bearer .+$/);
78
+ });
79
+
80
+ it("should handle various token formats", () => {
81
+ const tokens = [
82
+ "simple-token-123",
83
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0",
84
+ "ghp_1234567890abcdefghijklmnopqrstuv",
85
+ "Bearer_prefix_should_not_be_duplicated",
86
+ ];
87
+
88
+ tokens.forEach((token) => {
89
+ const header = `Bearer ${token}`;
90
+ expect(header).to.match(/^Bearer .+$/);
91
+ expect(header).to.equal(`Bearer ${token}`);
92
+ });
93
+ });
94
+ });
95
+
96
+ describe("Token Validation", () => {
97
+ it("should accept non-empty tokens", () => {
98
+ const validTokens = [
99
+ "abc123",
100
+ "very-long-token-with-many-characters-1234567890",
101
+ "token.with.dots",
102
+ "token_with_underscores",
103
+ ];
104
+
105
+ validTokens.forEach((token) => {
106
+ expect(token).to.be.a("string");
107
+ expect(token.length).to.be.greaterThan(0);
108
+ });
109
+ });
110
+ });
111
+ });
112
+
113
+ describe("API Key Authentication", () => {
114
+ describe("Custom Header Format", () => {
115
+ it("should create proper X-API-Key header", () => {
116
+ const headerName = "X-API-Key";
117
+ const apiKey = "abcdef123456";
118
+
119
+ expect(headerName).to.be.a("string");
120
+ expect(apiKey).to.be.a("string");
121
+ expect(headerName.length).to.be.greaterThan(0);
122
+ expect(apiKey.length).to.be.greaterThan(0);
123
+ });
124
+
125
+ it("should support various header names", () => {
126
+ const headers = [
127
+ { name: "X-API-Key", value: "key123" },
128
+ { name: "X-Auth-Token", value: "token456" },
129
+ { name: "API-Key", value: "apikey789" },
130
+ { name: "Authorization", value: "ApiKey xyz" },
131
+ { name: "X-Custom-Auth", value: "custom-value" },
132
+ ];
133
+
134
+ headers.forEach((header) => {
135
+ expect(header.name).to.be.a("string");
136
+ expect(header.value).to.be.a("string");
137
+ expect(header.name.length).to.be.greaterThan(0);
138
+ expect(header.value.length).to.be.greaterThan(0);
139
+ });
140
+ });
141
+ });
142
+
143
+ describe("API Key Validation", () => {
144
+ it("should accept alphanumeric keys", () => {
145
+ const validKeys = ["abc123", "API-KEY-12345", "key_with_underscores", "key.with.dots", "UPPERCASE_KEY"];
146
+
147
+ validKeys.forEach((key) => {
148
+ expect(key).to.be.a("string");
149
+ expect(key.length).to.be.greaterThan(0);
150
+ });
151
+ });
152
+ });
153
+ });
154
+
155
+ describe("Authentication Priority", () => {
156
+ it("should prioritize Bearer token over Basic auth", () => {
157
+ // When both are present, Bearer should be used (tested in implementation)
158
+ const basicAuthHeader = "Basic dGVzdDp0ZXN0";
159
+ const bearerAuthHeader = "Bearer token123";
160
+
161
+ expect(bearerAuthHeader).to.not.equal(basicAuthHeader);
162
+ expect(bearerAuthHeader).to.match(/^Bearer .+$/);
163
+ });
164
+
165
+ it("should allow API Key to coexist with Authorization header", () => {
166
+ // API Key uses custom header, so it doesn't conflict
167
+ const authHeader = "Authorization";
168
+ const apiKeyHeader = "X-API-Key";
169
+
170
+ expect(authHeader).to.not.equal(apiKeyHeader);
171
+ });
172
+ });
173
+
174
+ describe("Authentication Implementation Tests", () => {
175
+ describe("Bearer Token Authentication", () => {
176
+ it("should create correct header format with token", () => {
177
+ const token = "test-token-123";
178
+ const header = `Bearer ${token}`;
179
+
180
+ expect(header).to.equal("Bearer test-token-123");
181
+ expect(header).to.match(/^Bearer .+$/);
182
+ });
183
+
184
+ it("should handle whitespace trimming in token", () => {
185
+ const token = " test-token-456 ";
186
+ const trimmedToken = token.trim();
187
+ const header = `Bearer ${trimmedToken}`;
188
+
189
+ expect(header).to.equal("Bearer test-token-456");
190
+ expect(header).to.not.include(" ");
191
+ expect(trimmedToken).to.equal("test-token-456");
192
+ });
193
+
194
+ it("should validate empty token", () => {
195
+ const token = "";
196
+ const trimmedToken = token.trim();
197
+ const isValid = trimmedToken.length > 0;
198
+
199
+ expect(isValid).to.be.false;
200
+ });
201
+
202
+ it("should validate whitespace-only token", () => {
203
+ const token = " ";
204
+ const trimmedToken = token.trim();
205
+ const isValid = trimmedToken.length > 0;
206
+
207
+ expect(isValid).to.be.false;
208
+ });
209
+
210
+ it("should validate non-empty token", () => {
211
+ const token = "valid-token";
212
+ const trimmedToken = token.trim();
213
+ const isValid = trimmedToken.length > 0;
214
+
215
+ expect(isValid).to.be.true;
216
+ });
217
+ });
218
+
219
+ describe("API Key Authentication", () => {
220
+ it("should create correct custom header with API key", () => {
221
+ const headerName = "X-API-Key";
222
+ const apiKey = "my-api-key-123";
223
+ const headers: Record<string, string> = {};
224
+ headers[headerName] = apiKey;
225
+
226
+ expect(headers["X-API-Key"]).to.equal("my-api-key-123");
227
+ });
228
+
229
+ it("should handle whitespace trimming in header name and API key", () => {
230
+ const headerName = " X-Custom-Key ";
231
+ const apiKey = " key-value ";
232
+ const trimmedHeaderName = headerName.trim();
233
+ const trimmedApiKey = apiKey.trim();
234
+
235
+ const headers: Record<string, string> = {};
236
+ headers[trimmedHeaderName] = trimmedApiKey;
237
+
238
+ expect(headers["X-Custom-Key"]).to.equal("key-value");
239
+ expect(trimmedHeaderName).to.equal("X-Custom-Key");
240
+ expect(trimmedApiKey).to.equal("key-value");
241
+ });
242
+
243
+ it("should validate empty API key", () => {
244
+ const apiKey = "";
245
+ const trimmedApiKey = apiKey.trim();
246
+ const isValid = trimmedApiKey.length > 0;
247
+
248
+ expect(isValid).to.be.false;
249
+ });
250
+
251
+ it("should validate empty header name", () => {
252
+ const headerName = "";
253
+ const trimmedHeaderName = headerName.trim();
254
+ const isValid = trimmedHeaderName.length > 0;
255
+
256
+ expect(isValid).to.be.false;
257
+ });
258
+
259
+ it("should validate whitespace-only header name and key", () => {
260
+ const headerName = " ";
261
+ const apiKey = " ";
262
+ const isHeaderNameValid = headerName.trim().length > 0;
263
+ const isApiKeyValid = apiKey.trim().length > 0;
264
+
265
+ expect(isHeaderNameValid).to.be.false;
266
+ expect(isApiKeyValid).to.be.false;
267
+ });
268
+
269
+ it("should support various header names", () => {
270
+ const headerNames = ["X-API-Key", "X-Auth-Token", "API-Key", "X-Custom-Auth"];
271
+
272
+ headerNames.forEach((headerName) => {
273
+ const headers: Record<string, string> = {};
274
+ headers[headerName] = "test-key";
275
+ expect(headers[headerName]).to.equal("test-key");
276
+ });
277
+ });
278
+ });
279
+
280
+ describe("Basic Authentication", () => {
281
+ it("should create correct Basic auth header format", () => {
282
+ const username = "testuser";
283
+ const password = "testpass";
284
+ const credentials = `${username}:${password}`;
285
+ const encoded = btoa(credentials);
286
+ const header = `Basic ${encoded}`;
287
+
288
+ expect(header).to.match(/^Basic [A-Za-z0-9+/=]+$/);
289
+ expect(header).to.equal("Basic dGVzdHVzZXI6dGVzdHBhc3M=");
290
+ });
291
+
292
+ it("should validate credentials are provided", () => {
293
+ const username = "user";
294
+ const password = "pass";
295
+ const hasCredentials = !!(username && password);
296
+
297
+ expect(hasCredentials).to.be.true;
298
+ });
299
+
300
+ it("should detect missing credentials", () => {
301
+ const username = "";
302
+ const password = "pass";
303
+ const hasCredentials = !!(username && password);
304
+
305
+ expect(hasCredentials).to.be.false;
306
+ });
307
+ });
308
+
309
+ describe("Authentication Priority and Collision Detection", () => {
310
+ it("should verify Bearer takes priority over Basic by checking Authorization header usage", () => {
311
+ // Both Bearer and Basic use Authorization header, so only one can be used
312
+ const bearerToken = "bearer-token";
313
+ const bearerHeader = `Bearer ${bearerToken}`;
314
+ const basicHeader = "Basic " + btoa("user:pass");
315
+
316
+ // Verify they're different formats
317
+ expect(bearerHeader).to.not.equal(basicHeader);
318
+ expect(bearerHeader).to.include("Bearer");
319
+ expect(basicHeader).to.include("Basic");
320
+
321
+ // In implementation, Bearer is checked first and sets Authorization
322
+ // If Authorization exists, Basic won't overwrite it
323
+ });
324
+
325
+ it("should verify API Key can coexist with Bearer token using different headers", () => {
326
+ // API Key uses custom header, Bearer uses Authorization
327
+ const authorizationHeader = "Authorization";
328
+ const apiKeyHeader = "X-API-Key";
329
+
330
+ expect(authorizationHeader).to.not.equal(apiKeyHeader);
331
+ // These are different headers, so they can both be set
332
+ });
333
+
334
+ it("should verify API Key can coexist with Basic auth using different headers", () => {
335
+ // API Key uses custom header, Basic uses Authorization
336
+ const authorizationHeader = "Authorization";
337
+ const apiKeyHeader = "X-API-Key";
338
+
339
+ expect(authorizationHeader).to.not.equal(apiKeyHeader);
340
+ // These are different headers, so they can both be set
341
+ });
342
+
343
+ it("should validate header collision detection logic", () => {
344
+ const headers: Record<string, string> = { Authorization: "existing-value" };
345
+ const headerExists = headers["Authorization"] !== undefined;
346
+
347
+ expect(headerExists).to.be.true;
348
+
349
+ // Implementation should skip setting auth if header already exists
350
+ if (headerExists) {
351
+ // Don't overwrite
352
+ expect(headers["Authorization"]).to.equal("existing-value");
353
+ }
354
+ });
355
+
356
+ it("should validate custom header collision detection", () => {
357
+ const headers: Record<string, string> = { "X-API-Key": "existing-key" };
358
+ const headerExists = headers["X-API-Key"] !== undefined;
359
+
360
+ expect(headerExists).to.be.true;
361
+
362
+ // Implementation should skip setting API Key if header already exists
363
+ if (headerExists) {
364
+ // Don't overwrite
365
+ expect(headers["X-API-Key"]).to.equal("existing-key");
366
+ }
367
+ });
368
+ });
369
+
370
+ describe("Error Handling and Edge Cases", () => {
371
+ it("should validate undefined auth config handling", () => {
372
+ const bearerAuth = undefined;
373
+ const apiKeyAuth = undefined;
374
+
375
+ expect(bearerAuth).to.be.undefined;
376
+ expect(apiKeyAuth).to.be.undefined;
377
+
378
+ // Implementation should handle undefined gracefully
379
+ });
380
+
381
+ it("should validate null token handling", () => {
382
+ const token: any = null;
383
+ const isValid = !!(token && token.trim && token.trim().length > 0);
384
+
385
+ expect(isValid).to.be.false;
386
+ });
387
+
388
+ it("should validate function-based config pattern", () => {
389
+ // Verify that function returning config works
390
+ const getBearerConfig = () => ({ token: "dynamic-token" });
391
+ const getApiKeyConfig = () => ({ headerName: "X-Key", apiKey: "dynamic-key" });
392
+
393
+ const bearerConfig = getBearerConfig();
394
+ const apiKeyConfig = getApiKeyConfig();
395
+
396
+ expect(bearerConfig.token).to.equal("dynamic-token");
397
+ expect(apiKeyConfig.headerName).to.equal("X-Key");
398
+ expect(apiKeyConfig.apiKey).to.equal("dynamic-key");
399
+ });
400
+
401
+ it("should validate error handling in function-based config", () => {
402
+ const getAuthConfigWithError = () => {
403
+ throw new Error("Config error");
404
+ };
405
+
406
+ try {
407
+ getAuthConfigWithError();
408
+ // Should not reach here
409
+ expect.fail("Should have thrown error");
410
+ } catch (error) {
411
+ // Implementation should catch and handle this gracefully
412
+ expect(error).to.be.instanceOf(Error);
413
+ }
414
+ });
415
+ });
416
+
417
+ describe("Trimming Behavior Verification", () => {
418
+ it("should verify trimmed values are used in headers not originals", () => {
419
+ const originalToken = " token-with-spaces ";
420
+ const trimmedToken = originalToken.trim();
421
+
422
+ // The implementation should use trimmed token
423
+ const header = `Bearer ${trimmedToken}`;
424
+
425
+ expect(header).to.equal("Bearer token-with-spaces");
426
+ expect(header).to.not.include(" ");
427
+ });
428
+
429
+ it("should verify trimmed header name is used not original", () => {
430
+ const originalHeaderName = " X-API-Key ";
431
+ const trimmedHeaderName = originalHeaderName.trim();
432
+
433
+ const headers: Record<string, string> = {};
434
+ headers[trimmedHeaderName] = "value";
435
+
436
+ expect(headers["X-API-Key"]).to.equal("value");
437
+ expect(headers[" X-API-Key "]).to.be.undefined;
438
+ });
439
+
440
+ it("should verify trimmed API key is used not original", () => {
441
+ const originalKey = " key-value ";
442
+ const trimmedKey = originalKey.trim();
443
+
444
+ const headers: Record<string, string> = {};
445
+ headers["X-API-Key"] = trimmedKey;
446
+
447
+ expect(headers["X-API-Key"]).to.equal("key-value");
448
+ expect(headers["X-API-Key"]).to.not.include(" ");
449
+ });
450
+ });
451
+ });
452
+ });
package/src/defaults.ts CHANGED
@@ -175,6 +175,8 @@ SELECT * WHERE {
175
175
  withCredentials: false,
176
176
  adjustQueryBeforeRequest: false,
177
177
  basicAuth: undefined,
178
+ bearerAuth: undefined,
179
+ apiKeyAuth: undefined,
178
180
  };
179
181
  return { ...config, requestConfig };
180
182
  }
package/src/index.ts CHANGED
@@ -442,11 +442,8 @@ export class Yasqe extends CodeMirror {
442
442
  }
443
443
 
444
444
  private drawSnippetsBar() {
445
- // Check if snippets bar should be shown
446
- const shouldShow =
447
- this.config.showSnippetsBar &&
448
- (this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
449
- this.config.snippets.length > 0;
445
+ // Check if snippets bar should be shown (now using global config only)
446
+ const shouldShow = this.config.showSnippetsBar && this.config.snippets.length > 0;
450
447
 
451
448
  if (!shouldShow) {
452
449
  // Remove existing bar if present
@@ -1126,25 +1123,17 @@ export class Yasqe extends CodeMirror {
1126
1123
  * Snippets management
1127
1124
  */
1128
1125
  public setSnippetsBarVisible(visible: boolean) {
1129
- if (!this.persistentConfig) {
1130
- this.persistentConfig = {
1131
- query: this.getValue(),
1132
- editorHeight: this.config.editorHeight,
1133
- showSnippetsBar: visible,
1134
- };
1135
- } else {
1136
- this.persistentConfig.showSnippetsBar = visible;
1137
- }
1138
- this.saveQuery();
1126
+ // Update config and redraw
1127
+ this.config.showSnippetsBar = visible;
1139
1128
  this.drawSnippetsBar();
1140
1129
  }
1141
1130
 
1142
1131
  public getSnippetsBarVisible(): boolean {
1143
- return (
1144
- this.config.showSnippetsBar &&
1145
- (this.persistentConfig?.showSnippetsBar === undefined || this.persistentConfig.showSnippetsBar) &&
1146
- this.config.snippets.length > 0
1147
- );
1132
+ return this.config.showSnippetsBar && this.config.snippets.length > 0;
1133
+ }
1134
+
1135
+ public refreshSnippetsBar() {
1136
+ this.drawSnippetsBar();
1148
1137
  }
1149
1138
 
1150
1139
  /**
@@ -1365,6 +1354,14 @@ export interface BasicAuthConfig {
1365
1354
  username: string;
1366
1355
  password: string;
1367
1356
  }
1357
+ export interface BearerAuthConfig {
1358
+ token: string;
1359
+ }
1360
+ export interface ApiKeyAuthConfig {
1361
+ headerName: string;
1362
+ apiKey: string;
1363
+ }
1364
+ export type AuthConfig = BasicAuthConfig | BearerAuthConfig | ApiKeyAuthConfig;
1368
1365
  export interface RequestConfig<Y> {
1369
1366
  queryArgument: string | ((yasqe: Y) => string) | undefined;
1370
1367
  endpoint: string | ((yasqe: Y) => string);
@@ -1379,6 +1376,8 @@ export interface RequestConfig<Y> {
1379
1376
  withCredentials: boolean | ((yasqe: Y) => boolean);
1380
1377
  adjustQueryBeforeRequest: ((yasqe: Y) => string) | false;
1381
1378
  basicAuth: BasicAuthConfig | ((yasqe: Y) => BasicAuthConfig | undefined) | undefined;
1379
+ bearerAuth: BearerAuthConfig | ((yasqe: Y) => BearerAuthConfig | undefined) | undefined;
1380
+ apiKeyAuth: ApiKeyAuthConfig | ((yasqe: Y) => ApiKeyAuthConfig | undefined) | undefined;
1382
1381
  }
1383
1382
  export type PlainRequestConfig = {
1384
1383
  [K in keyof RequestConfig<any>]: Exclude<RequestConfig<any>[K], Function>;
@@ -1439,7 +1438,6 @@ export interface PersistentConfig {
1439
1438
  editorHeight: string;
1440
1439
  formatterType?: "sparql-formatter" | "legacy"; // Which formatter to use
1441
1440
  autoformatOnQuery?: boolean; // Auto-format query on execution
1442
- showSnippetsBar?: boolean; // User preference for snippets bar visibility
1443
1441
  }
1444
1442
  // export var _Yasqe = _Yasqe;
1445
1443