Skip to content

Commit bdfee49

Browse files
committed
fix(auth): add base64 encoding to Basic Auth and validate empty apiKey
- HttpClient now properly encodes API key in base64 for Basic Auth header - NfeClient validates that apiKey is not empty or whitespace-only string - Remove fake timers from http-client tests (causing timeouts in CI) - Replace Map with createMockHeaders helper for proper Headers API mocking - Use shorter retry delays (10ms) in tests for faster execution Fixes CI test failures: - 'Basic test-api-key' -> 'Basic dGVzdC1hcGkta2V5...' (base64) - Empty apiKey '' now throws ConfigurationError - Tests no longer timeout due to fake timers blocking promises
1 parent d93d6ce commit bdfee49

3 files changed

Lines changed: 42 additions & 58 deletions

File tree

src/core/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,10 @@ export class NfeClient {
286286
// --------------------------------------------------------------------------
287287

288288
private validateAndNormalizeConfig(config: NfeConfig): RequiredNfeConfig {
289-
if (!config.apiKey) {
289+
if (!config.apiKey || config.apiKey.trim() === '') {
290290
// Try to get from environment variable
291291
const envApiKey = this.getEnvironmentVariable('NFE_API_KEY');
292-
if (!envApiKey) {
292+
if (!envApiKey || envApiKey.trim() === '') {
293293
throw ErrorFactory.fromMissingApiKey();
294294
}
295295
config.apiKey = envApiKey;

src/core/http/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export class HttpClient {
254254

255255
private buildHeaders(data?: unknown): Record<string, string> {
256256
const headers: Record<string, string> = {
257-
'Authorization': `Basic ${this.config.apiKey}`,
257+
'Authorization': `Basic ${Buffer.from(this.config.apiKey).toString('base64')}`,
258258
'Accept': 'application/json',
259259
'User-Agent': this.getUserAgent(),
260260
};

tests/unit/http-client.test.ts

Lines changed: 39 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@ import { HttpClient, createDefaultRetryConfig, buildHttpConfig } from '../../src
33
import type { HttpConfig } from '../../src/core/types';
44
import { TEST_API_KEY } from '../setup';
55

6+
// Helper to create mock Headers object
7+
function createMockHeaders(entries: [string, string][]): any {
8+
const map = new Map(entries);
9+
return {
10+
get: (key: string) => map.get(key.toLowerCase()) || null,
11+
has: (key: string) => map.has(key.toLowerCase()),
12+
entries: () => map.entries(),
13+
keys: () => map.keys(),
14+
values: () => map.values(),
15+
};
16+
}
17+
618
describe('HttpClient', () => {
719
let httpClient: HttpClient;
820
let fetchMock: ReturnType<typeof vi.fn>;
921
let config: HttpConfig;
1022

1123
beforeEach(() => {
12-
// Use fake timers to speed up retry tests
13-
vi.useFakeTimers();
14-
1524
config = buildHttpConfig(
1625
TEST_API_KEY,
1726
'https://api.nfe.io/v1',
1827
10000,
19-
createDefaultRetryConfig()
28+
{ maxRetries: 3, baseDelay: 10, maxDelay: 100 } // Delays curtos para testes rápidos
2029
);
2130

2231
httpClient = new HttpClient(config);
@@ -27,7 +36,6 @@ describe('HttpClient', () => {
2736
});
2837

2938
afterEach(() => {
30-
vi.useRealTimers();
3139
vi.restoreAllMocks();
3240
});
3341

@@ -38,7 +46,7 @@ describe('HttpClient', () => {
3846
ok: true,
3947
status: 200,
4048
statusText: 'OK',
41-
headers: new Map([['content-type', 'application/json']]),
49+
headers: createMockHeaders([['content-type', 'application/json']]),
4250
json: async () => mockData,
4351
});
4452

@@ -58,7 +66,7 @@ describe('HttpClient', () => {
5866
fetchMock.mockResolvedValue({
5967
ok: true,
6068
status: 200,
61-
headers: new Map([['content-type', 'application/json']]),
69+
headers: createMockHeaders([['content-type', 'application/json']]),
6270
json: async () => ([]),
6371
});
6472

@@ -73,7 +81,7 @@ describe('HttpClient', () => {
7381
fetchMock.mockResolvedValue({
7482
ok: true,
7583
status: 200,
76-
headers: new Map([['content-type', 'application/json']]),
84+
headers: createMockHeaders([['content-type', 'application/json']]),
7785
json: async () => ([]),
7886
});
7987

@@ -99,7 +107,7 @@ describe('HttpClient', () => {
99107
ok: true,
100108
status: 201,
101109
statusText: 'Created',
102-
headers: new Map([['content-type', 'application/json']]),
110+
headers: createMockHeaders([['content-type', 'application/json']]),
103111
json: async () => responseBody,
104112
});
105113

@@ -146,7 +154,7 @@ describe('HttpClient', () => {
146154
fetchMock.mockResolvedValue({
147155
ok: true,
148156
status: 200,
149-
headers: new Map([['content-type', 'application/json']]),
157+
headers: createMockHeaders([['content-type', 'application/json']]),
150158
json: async () => responseBody,
151159
});
152160

@@ -179,7 +187,7 @@ describe('HttpClient', () => {
179187
fetchMock.mockResolvedValue({
180188
ok: true,
181189
status: 200,
182-
headers: new Map([['content-type', 'application/json']]),
190+
headers: createMockHeaders([['content-type', 'application/json']]),
183191
json: async () => ({}),
184192
});
185193

@@ -194,7 +202,7 @@ describe('HttpClient', () => {
194202
ok: false,
195203
status: 401,
196204
statusText: 'Unauthorized',
197-
headers: new Map([['content-type', 'application/json']]),
205+
headers: createMockHeaders([['content-type', 'application/json']]),
198206
json: async () => ({ error: 'Invalid API key' }),
199207
});
200208

@@ -215,7 +223,7 @@ describe('HttpClient', () => {
215223
ok: false,
216224
status: 400,
217225
statusText: 'Bad Request',
218-
headers: new Map([['content-type', 'application/json']]),
226+
headers: createMockHeaders([['content-type', 'application/json']]),
219227
json: async () => ({
220228
error: 'Validation failed',
221229
details: { field: 'required' },
@@ -236,7 +244,7 @@ describe('HttpClient', () => {
236244
ok: false,
237245
status: 404,
238246
statusText: 'Not Found',
239-
headers: new Map([['content-type', 'application/json']]),
247+
headers: createMockHeaders([['content-type', 'application/json']]),
240248
json: async () => ({ error: 'Resource not found' }),
241249
});
242250

@@ -264,9 +272,6 @@ describe('HttpClient', () => {
264272

265273
const promise = httpClient.get('/test');
266274

267-
// Rate limits are retried, so advance timers to complete all retries
268-
await vi.runAllTimersAsync();
269-
270275
// Should fail after max retries
271276
await expect(promise).rejects.toMatchObject({
272277
name: 'RateLimitError',
@@ -283,15 +288,12 @@ describe('HttpClient', () => {
283288
ok: false,
284289
status: 500,
285290
statusText: 'Internal Server Error',
286-
headers: new Map([['content-type', 'application/json']]),
291+
headers: createMockHeaders([['content-type', 'application/json']]),
287292
json: async () => ({ error: 'Server error' }),
288293
});
289294

290295
const promise = httpClient.get('/test');
291296

292-
// Server errors are retried, so advance timers
293-
await vi.runAllTimersAsync();
294-
295297
// Should fail after max retries
296298
await expect(promise).rejects.toMatchObject({
297299
name: 'ServerError',
@@ -307,9 +309,6 @@ describe('HttpClient', () => {
307309

308310
const promise = httpClient.get('/test');
309311

310-
// Network errors are retried, so advance timers
311-
await vi.runAllTimersAsync();
312-
313312
// Should fail after max retries
314313
await expect(promise).rejects.toMatchObject({
315314
name: 'ConnectionError',
@@ -326,9 +325,6 @@ describe('HttpClient', () => {
326325

327326
const promise = httpClient.get('/test');
328327

329-
// Timeout errors are retried, so advance timers
330-
await vi.runAllTimersAsync();
331-
332328
// Should fail after max retries
333329
await expect(promise).rejects.toMatchObject({
334330
name: 'TimeoutError',
@@ -345,28 +341,25 @@ describe('HttpClient', () => {
345341
ok: false,
346342
status: 503,
347343
statusText: 'Service Unavailable',
348-
headers: new Map([['content-type', 'application/json']]),
344+
headers: createMockHeaders([['content-type', 'application/json']]),
349345
json: async () => ({ error: 'Temporarily unavailable' }),
350346
})
351347
.mockResolvedValueOnce({
352348
ok: false,
353349
status: 503,
354350
statusText: 'Service Unavailable',
355-
headers: new Map([['content-type', 'application/json']]),
351+
headers: createMockHeaders([['content-type', 'application/json']]),
356352
json: async () => ({ error: 'Temporarily unavailable' }),
357353
})
358354
.mockResolvedValueOnce({
359355
ok: true,
360356
status: 200,
361-
headers: new Map([['content-type', 'application/json']]),
357+
headers: createMockHeaders([['content-type', 'application/json']]),
362358
json: async () => ({ success: true }),
363359
});
364360

365361
const promise = httpClient.get<{ success: boolean }>('/test');
366362

367-
// Fast-forward through retry delays
368-
await vi.runAllTimersAsync();
369-
370363
const response = await promise;
371364

372365
expect(response.data).toEqual({ success: true });
@@ -380,15 +373,12 @@ describe('HttpClient', () => {
380373
.mockResolvedValueOnce({
381374
ok: true,
382375
status: 200,
383-
headers: new Map([['content-type', 'application/json']]),
376+
headers: createMockHeaders([['content-type', 'application/json']]),
384377
json: async () => ({ success: true }),
385378
});
386379

387380
const promise = httpClient.get<{ success: boolean }>('/test');
388381

389-
// Fast-forward through retry delays
390-
await vi.runAllTimersAsync();
391-
392382
const response = await promise;
393383

394384
expect(response.data).toEqual({ success: true });
@@ -400,7 +390,7 @@ describe('HttpClient', () => {
400390
ok: false,
401391
status: 400,
402392
statusText: 'Bad Request',
403-
headers: new Map([['content-type', 'application/json']]),
393+
headers: createMockHeaders([['content-type', 'application/json']]),
404394
json: async () => ({ error: 'Invalid input' }),
405395
});
406396

@@ -415,15 +405,12 @@ describe('HttpClient', () => {
415405
ok: false,
416406
status: 503,
417407
statusText: 'Service Unavailable',
418-
headers: new Map([['content-type', 'application/json']]),
408+
headers: createMockHeaders([['content-type', 'application/json']]),
419409
json: async () => ({ error: 'Unavailable' }),
420410
});
421411

422412
const promise = httpClient.get('/test');
423413

424-
// Fast-forward through all retry attempts
425-
await vi.runAllTimersAsync();
426-
427414
await expect(promise).rejects.toThrow();
428415
// Initial request + 3 retries = 4 total
429416
expect(fetchMock).toHaveBeenCalledTimes(4);
@@ -443,15 +430,12 @@ describe('HttpClient', () => {
443430
.mockResolvedValueOnce({
444431
ok: true,
445432
status: 200,
446-
headers: new Map([['content-type', 'application/json']]),
433+
headers: createMockHeaders([['content-type', 'application/json']]),
447434
json: async () => ({ success: true }),
448435
});
449436

450437
const promise = httpClient.get<{ success: boolean }>('/test');
451438

452-
// Fast-forward through retry delay
453-
await vi.runAllTimersAsync();
454-
455439
const response = await promise;
456440
expect(response.data).toEqual({ success: true });
457441
expect(fetchMock).toHaveBeenCalledTimes(2);
@@ -463,7 +447,7 @@ describe('HttpClient', () => {
463447
fetchMock.mockResolvedValue({
464448
ok: true,
465449
status: 200,
466-
headers: new Map([['content-type', 'application/json']]),
450+
headers: createMockHeaders([['content-type', 'application/json']]),
467451
json: async () => ({}),
468452
});
469453

@@ -486,7 +470,7 @@ describe('HttpClient', () => {
486470
fetchMock.mockResolvedValue({
487471
ok: true,
488472
status: 200,
489-
headers: new Map([['content-type', 'application/json']]),
473+
headers: createMockHeaders([['content-type', 'application/json']]),
490474
json: async () => ({}),
491475
});
492476

@@ -503,7 +487,7 @@ describe('HttpClient', () => {
503487
fetchMock.mockResolvedValue({
504488
ok: true,
505489
status: 200,
506-
headers: new Map([['content-type', 'application/json']]),
490+
headers: createMockHeaders([['content-type', 'application/json']]),
507491
json: async () => jsonData,
508492
});
509493

@@ -515,7 +499,7 @@ describe('HttpClient', () => {
515499
fetchMock.mockResolvedValue({
516500
ok: true,
517501
status: 200,
518-
headers: new Map([['content-type', 'text/plain']]),
502+
headers: createMockHeaders([['content-type', 'text/plain']]),
519503
text: async () => 'Plain text response',
520504
});
521505

@@ -530,7 +514,7 @@ describe('HttpClient', () => {
530514
fetchMock.mockResolvedValue({
531515
ok: true,
532516
status: 200,
533-
headers: new Map([['content-type', 'application/pdf']]),
517+
headers: createMockHeaders([['content-type', 'application/pdf']]),
534518
arrayBuffer: async () => arrayBuffer,
535519
});
536520

@@ -547,7 +531,7 @@ describe('HttpClient', () => {
547531
fetchMock.mockResolvedValue({
548532
ok: true,
549533
status: 200,
550-
headers: new Map([['content-type', 'application/xml']]),
534+
headers: createMockHeaders([['content-type', 'application/xml']]),
551535
arrayBuffer: async () => arrayBuffer,
552536
});
553537

@@ -563,7 +547,7 @@ describe('HttpClient', () => {
563547
fetchMock.mockResolvedValue({
564548
ok: true,
565549
status: 200,
566-
headers: new Map([['content-type', 'application/json']]),
550+
headers: createMockHeaders([['content-type', 'application/json']]),
567551
json: async () => ({}),
568552
});
569553

@@ -578,7 +562,7 @@ describe('HttpClient', () => {
578562
fetchMock.mockResolvedValue({
579563
ok: true,
580564
status: 200,
581-
headers: new Map([['content-type', 'application/json']]),
565+
headers: createMockHeaders([['content-type', 'application/json']]),
582566
json: async () => ({}),
583567
});
584568

@@ -592,7 +576,7 @@ describe('HttpClient', () => {
592576
fetchMock.mockResolvedValue({
593577
ok: true,
594578
status: 201,
595-
headers: new Map([['content-type', 'application/json']]),
579+
headers: createMockHeaders([['content-type', 'application/json']]),
596580
json: async () => ({}),
597581
});
598582

0 commit comments

Comments
 (0)