@paakd/api 0.0.1
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/dist/src/index.js +21 -0
- package/package.json +59 -0
- package/src/address.spec.ts +662 -0
- package/src/address.ts +300 -0
- package/src/auth.spec.ts +771 -0
- package/src/auth.ts +168 -0
- package/src/compressor/brotli.ts +26 -0
- package/src/index.ts +5 -0
- package/src/interceptors.spec.ts +1343 -0
- package/src/interceptors.ts +224 -0
- package/src/policies.spec.ts +595 -0
- package/src/policies.ts +431 -0
- package/src/products.spec.ts +710 -0
- package/src/products.ts +112 -0
- package/src/profile.spec.ts +626 -0
- package/src/profile.ts +169 -0
- package/src/proto/auth/v1/entities/auth.proto +140 -0
- package/src/proto/auth/v1/entities/policy.proto +57 -0
- package/src/proto/auth/v1/service.proto +26 -0
- package/src/proto/customers/v1/entities/address.proto +101 -0
- package/src/proto/customers/v1/entities/profile.proto +118 -0
- package/src/proto/customers/v1/service.proto +36 -0
- package/src/proto/files/v1/entities/file.proto +62 -0
- package/src/proto/files/v1/service.proto +19 -0
- package/src/proto/products/v1/entities/category.proto +98 -0
- package/src/proto/products/v1/entities/collection.proto +72 -0
- package/src/proto/products/v1/entities/product/create.proto +41 -0
- package/src/proto/products/v1/entities/product/option.proto +17 -0
- package/src/proto/products/v1/entities/product/shared.proto +255 -0
- package/src/proto/products/v1/entities/product/update.proto +66 -0
- package/src/proto/products/v1/entities/tag.proto +73 -0
- package/src/proto/products/v1/entities/taxonomy.proto +146 -0
- package/src/proto/products/v1/entities/type.proto +98 -0
- package/src/proto/products/v1/entities/variant.proto +127 -0
- package/src/proto/products/v1/service.proto +78 -0
- package/src/proto/promotions/v1/entities/campaign.proto +145 -0
- package/src/proto/promotions/v1/service.proto +17 -0
- package/src/proto/stocknodes/v1/entities/stocknode.proto +167 -0
- package/src/proto/stocknodes/v1/service.proto +21 -0
- package/src/registration.ts +170 -0
- package/src/test-utils.ts +176 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
StreamRequest,
|
|
3
|
+
StreamResponse,
|
|
4
|
+
UnaryRequest,
|
|
5
|
+
UnaryResponse,
|
|
6
|
+
} from '@connectrpc/connect'
|
|
7
|
+
import { Code, ConnectError } from '@connectrpc/connect'
|
|
8
|
+
import { type MockedFunction } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
createAuthenticationInterceptor,
|
|
11
|
+
createCustomerAuthenticationInterceptor,
|
|
12
|
+
createHeadersInterceptor,
|
|
13
|
+
createLoggingInterceptor,
|
|
14
|
+
type RequestLogger,
|
|
15
|
+
} from './interceptors'
|
|
16
|
+
|
|
17
|
+
// Mock the checkout config
|
|
18
|
+
vi.mock('@paakd/config', () => ({
|
|
19
|
+
Checkout: {} as any,
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('./compressor/brotli', () => ({
|
|
23
|
+
brotliCompression: {
|
|
24
|
+
name: 'brotli',
|
|
25
|
+
compress: vi.fn(),
|
|
26
|
+
decompress: vi.fn(),
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
describe('interceptors', () => {
|
|
31
|
+
describe('createLoggingInterceptor', () => {
|
|
32
|
+
let mockLogger: RequestLogger
|
|
33
|
+
let mockNext: MockedFunction<
|
|
34
|
+
(
|
|
35
|
+
req: UnaryRequest | StreamRequest
|
|
36
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
37
|
+
>
|
|
38
|
+
let mockRequest: any
|
|
39
|
+
let mockResponse: any
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
mockLogger = {
|
|
43
|
+
onRequestHeader: vi.fn(),
|
|
44
|
+
onRequestMessage: vi.fn(),
|
|
45
|
+
onResponseHeader: vi.fn(),
|
|
46
|
+
onResponseMessage: vi.fn(),
|
|
47
|
+
onResponseTrailer: vi.fn(),
|
|
48
|
+
onError: vi.fn(),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
mockNext = vi.fn() as MockedFunction<
|
|
52
|
+
(
|
|
53
|
+
req: UnaryRequest | StreamRequest
|
|
54
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
55
|
+
>
|
|
56
|
+
|
|
57
|
+
mockRequest = {
|
|
58
|
+
stream: false,
|
|
59
|
+
method: {
|
|
60
|
+
methodKind: 'unary',
|
|
61
|
+
toString: () => 'TestService/TestMethod',
|
|
62
|
+
},
|
|
63
|
+
header: new Headers(),
|
|
64
|
+
message: { $typeName: 'test.Message' },
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mockResponse = {
|
|
68
|
+
stream: false,
|
|
69
|
+
header: new Headers(),
|
|
70
|
+
message: { $typeName: 'test.Message' },
|
|
71
|
+
trailer: new Headers(),
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should create a logging interceptor with default logger', () => {
|
|
76
|
+
const interceptor = createLoggingInterceptor()
|
|
77
|
+
expect(typeof interceptor).toBe('function')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should create a logging interceptor with custom logger', () => {
|
|
81
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
82
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
83
|
+
expect(typeof interceptor).toBe('function')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('should log unary request and response', async () => {
|
|
87
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
88
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
89
|
+
const interceptorFn = interceptor(mockNext)
|
|
90
|
+
|
|
91
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
92
|
+
|
|
93
|
+
const result = await interceptorFn(mockRequest)
|
|
94
|
+
|
|
95
|
+
expect(customLogger).toHaveBeenCalledWith(mockRequest)
|
|
96
|
+
expect(mockLogger.onRequestHeader).toHaveBeenCalledWith(
|
|
97
|
+
mockRequest.header
|
|
98
|
+
)
|
|
99
|
+
expect(mockLogger.onRequestMessage).toHaveBeenCalledWith(
|
|
100
|
+
mockRequest.message
|
|
101
|
+
)
|
|
102
|
+
expect(mockLogger.onResponseHeader).toHaveBeenCalledWith(
|
|
103
|
+
mockResponse.header
|
|
104
|
+
)
|
|
105
|
+
expect(mockLogger.onResponseMessage).toHaveBeenCalledWith(
|
|
106
|
+
mockResponse.message
|
|
107
|
+
)
|
|
108
|
+
expect(mockLogger.onResponseTrailer).toHaveBeenCalledWith(
|
|
109
|
+
mockResponse.trailer
|
|
110
|
+
)
|
|
111
|
+
expect(result).toBe(mockResponse)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('should handle stream requests with multiple messages', async () => {
|
|
115
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
116
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
117
|
+
const interceptorFn = interceptor(mockNext)
|
|
118
|
+
|
|
119
|
+
const messages = [
|
|
120
|
+
{ $typeName: 'test.Message', id: 1 },
|
|
121
|
+
{ $typeName: 'test.Message', id: 2 },
|
|
122
|
+
{ $typeName: 'test.Message', id: 3 },
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const streamRequest = {
|
|
126
|
+
...mockRequest,
|
|
127
|
+
stream: true,
|
|
128
|
+
message: (async function* () {
|
|
129
|
+
for (const msg of messages) {
|
|
130
|
+
yield msg
|
|
131
|
+
}
|
|
132
|
+
})(),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const streamResponse = {
|
|
136
|
+
...mockResponse,
|
|
137
|
+
stream: true,
|
|
138
|
+
message: (async function* () {
|
|
139
|
+
for (const msg of messages) {
|
|
140
|
+
yield msg
|
|
141
|
+
}
|
|
142
|
+
})(),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
mockNext.mockResolvedValue(streamResponse)
|
|
146
|
+
|
|
147
|
+
const result = await interceptorFn(streamRequest as any)
|
|
148
|
+
|
|
149
|
+
expect(customLogger).toHaveBeenCalledWith(streamRequest)
|
|
150
|
+
expect(mockLogger.onRequestHeader).toHaveBeenCalledWith(
|
|
151
|
+
streamRequest.header
|
|
152
|
+
)
|
|
153
|
+
expect(result).toBeDefined()
|
|
154
|
+
expect(result.stream).toBe(true)
|
|
155
|
+
|
|
156
|
+
// Consume the stream to trigger message logging
|
|
157
|
+
const responseMessages = []
|
|
158
|
+
for await (const msg of result.message as AsyncIterable<any>) {
|
|
159
|
+
responseMessages.push(msg)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
expect(responseMessages).toHaveLength(3)
|
|
163
|
+
expect(mockLogger.onResponseMessage).toHaveBeenCalledTimes(3)
|
|
164
|
+
expect(mockLogger.onResponseTrailer).toHaveBeenCalledWith(
|
|
165
|
+
mockResponse.trailer
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should log errors in stream and rethrow', async () => {
|
|
170
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
171
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
172
|
+
const interceptorFn = interceptor(mockNext)
|
|
173
|
+
|
|
174
|
+
const streamError = new Error('Stream processing error')
|
|
175
|
+
|
|
176
|
+
const streamRequest = {
|
|
177
|
+
...mockRequest,
|
|
178
|
+
stream: true,
|
|
179
|
+
message: (async function* () {
|
|
180
|
+
yield { $typeName: 'test.Message' }
|
|
181
|
+
throw streamError
|
|
182
|
+
})(),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const streamResponse = {
|
|
186
|
+
...mockResponse,
|
|
187
|
+
stream: true,
|
|
188
|
+
message: (async function* () {
|
|
189
|
+
throw streamError
|
|
190
|
+
})(),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
mockNext.mockResolvedValue(streamResponse)
|
|
194
|
+
|
|
195
|
+
const result = await interceptorFn(streamRequest as any)
|
|
196
|
+
|
|
197
|
+
// Consume the response stream to trigger error
|
|
198
|
+
await expect(async () => {
|
|
199
|
+
for await (const msg of result.message as AsyncIterable<any>) {
|
|
200
|
+
// Iterate through messages
|
|
201
|
+
}
|
|
202
|
+
}).rejects.toThrow('Stream processing error')
|
|
203
|
+
|
|
204
|
+
expect(mockLogger.onError).toHaveBeenCalledWith(streamError)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('should log errors and rethrow them in unary calls', async () => {
|
|
208
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
209
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
210
|
+
const interceptorFn = interceptor(mockNext)
|
|
211
|
+
|
|
212
|
+
const error = new Error('Test error')
|
|
213
|
+
mockNext.mockRejectedValue(error)
|
|
214
|
+
|
|
215
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow('Test error')
|
|
216
|
+
expect(mockLogger.onError).toHaveBeenCalledWith(error)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should log ConnectError during request processing', async () => {
|
|
220
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
221
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
222
|
+
const interceptorFn = interceptor(mockNext)
|
|
223
|
+
|
|
224
|
+
const connectError = new ConnectError(
|
|
225
|
+
'Service unavailable',
|
|
226
|
+
Code.Unavailable
|
|
227
|
+
)
|
|
228
|
+
mockNext.mockRejectedValue(connectError)
|
|
229
|
+
|
|
230
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
231
|
+
expect(mockLogger.onError).toHaveBeenCalledWith(connectError)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should use default logger when none provided', async () => {
|
|
235
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
236
|
+
|
|
237
|
+
const interceptor = createLoggingInterceptor()
|
|
238
|
+
const interceptorFn = interceptor(mockNext)
|
|
239
|
+
|
|
240
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
241
|
+
|
|
242
|
+
await interceptorFn(mockRequest)
|
|
243
|
+
|
|
244
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
245
|
+
const calls = consoleSpy.mock.calls.filter(call =>
|
|
246
|
+
call[0]?.includes('TestService')
|
|
247
|
+
)
|
|
248
|
+
expect(calls.length).toBeGreaterThan(0)
|
|
249
|
+
|
|
250
|
+
consoleSpy.mockRestore()
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should handle partial logger implementation', async () => {
|
|
254
|
+
const partialLogger: RequestLogger = {
|
|
255
|
+
onRequestHeader: vi.fn(),
|
|
256
|
+
// Missing other callbacks
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const customLogger = vi.fn().mockReturnValue(partialLogger)
|
|
260
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
261
|
+
const interceptorFn = interceptor(mockNext)
|
|
262
|
+
|
|
263
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
264
|
+
|
|
265
|
+
// Should not throw even with missing callbacks
|
|
266
|
+
await expect(interceptorFn(mockRequest)).resolves.toBeDefined()
|
|
267
|
+
expect(partialLogger.onRequestHeader).toHaveBeenCalled()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should pass correct method info to logger function', async () => {
|
|
271
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
272
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
273
|
+
const interceptorFn = interceptor(mockNext)
|
|
274
|
+
|
|
275
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
276
|
+
|
|
277
|
+
const testRequest = {
|
|
278
|
+
...mockRequest,
|
|
279
|
+
method: {
|
|
280
|
+
methodKind: 'unary',
|
|
281
|
+
toString: () => 'CustomService/CustomMethod',
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
await interceptorFn(testRequest)
|
|
286
|
+
|
|
287
|
+
expect(customLogger).toHaveBeenCalledWith(testRequest)
|
|
288
|
+
const loggerResult = customLogger.mock.results[0].value
|
|
289
|
+
expect(loggerResult).toBeDefined()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should handle stream response completion without error', async () => {
|
|
293
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
294
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
295
|
+
const interceptorFn = interceptor(mockNext)
|
|
296
|
+
|
|
297
|
+
const streamResponse = {
|
|
298
|
+
...mockResponse,
|
|
299
|
+
stream: true,
|
|
300
|
+
message: (async function* () {
|
|
301
|
+
yield { $typeName: 'test.Message', id: 1 }
|
|
302
|
+
// Stream ends normally
|
|
303
|
+
})(),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
mockNext.mockResolvedValue(streamResponse)
|
|
307
|
+
|
|
308
|
+
const result = await interceptorFn(mockRequest)
|
|
309
|
+
|
|
310
|
+
// Consume the stream
|
|
311
|
+
const messages = []
|
|
312
|
+
for await (const msg of result.message as AsyncIterable<any>) {
|
|
313
|
+
messages.push(msg)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
expect(messages).toHaveLength(1)
|
|
317
|
+
expect(mockLogger.onResponseTrailer).toHaveBeenCalled()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('should handle stream iterator with throw method', async () => {
|
|
321
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
322
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
323
|
+
const interceptorFn = interceptor(mockNext)
|
|
324
|
+
|
|
325
|
+
const mockIterator = {
|
|
326
|
+
next: vi.fn().mockResolvedValue({
|
|
327
|
+
done: false,
|
|
328
|
+
value: { $typeName: 'test.Message' },
|
|
329
|
+
}),
|
|
330
|
+
throw: vi.fn().mockRejectedValue(new Error('Thrown error')),
|
|
331
|
+
return: vi.fn().mockResolvedValue({ done: true }),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const streamResponse = {
|
|
335
|
+
...mockResponse,
|
|
336
|
+
stream: true,
|
|
337
|
+
message: {
|
|
338
|
+
[Symbol.asyncIterator]: () => mockIterator,
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
mockNext.mockResolvedValue(streamResponse)
|
|
343
|
+
|
|
344
|
+
const result = await interceptorFn(mockRequest as any)
|
|
345
|
+
|
|
346
|
+
// Get the intercepted iterator
|
|
347
|
+
const interceptedIterator = (result.message as AsyncIterable<any>)[
|
|
348
|
+
Symbol.asyncIterator
|
|
349
|
+
]()
|
|
350
|
+
|
|
351
|
+
// The intercepted iterator should have throw and return methods
|
|
352
|
+
expect(typeof interceptedIterator.throw).toBe('function')
|
|
353
|
+
expect(typeof interceptedIterator.return).toBe('function')
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it('should handle stream iterator without throw method', async () => {
|
|
357
|
+
const customLogger = vi.fn().mockReturnValue(mockLogger)
|
|
358
|
+
const interceptor = createLoggingInterceptor(customLogger)
|
|
359
|
+
const interceptorFn = interceptor(mockNext)
|
|
360
|
+
|
|
361
|
+
const mockIterator = {
|
|
362
|
+
next: vi.fn().mockResolvedValue({
|
|
363
|
+
done: false,
|
|
364
|
+
value: { $typeName: 'test.Message' },
|
|
365
|
+
}),
|
|
366
|
+
// No throw or return methods
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const streamResponse = {
|
|
370
|
+
...mockResponse,
|
|
371
|
+
stream: true,
|
|
372
|
+
message: {
|
|
373
|
+
[Symbol.asyncIterator]: () => mockIterator,
|
|
374
|
+
},
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
mockNext.mockResolvedValue(streamResponse)
|
|
378
|
+
|
|
379
|
+
const result = await interceptorFn(mockRequest as any)
|
|
380
|
+
|
|
381
|
+
const interceptedIterator = (result.message as AsyncIterable<any>)[
|
|
382
|
+
Symbol.asyncIterator
|
|
383
|
+
]()
|
|
384
|
+
|
|
385
|
+
// Should work without throw/return methods
|
|
386
|
+
const msg = await interceptedIterator.next()
|
|
387
|
+
expect(msg.value).toBeDefined()
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
describe('createAuthenticationInterceptor', () => {
|
|
392
|
+
let mockNext: MockedFunction<
|
|
393
|
+
(
|
|
394
|
+
req: UnaryRequest | StreamRequest
|
|
395
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
396
|
+
>
|
|
397
|
+
let mockRequest: any
|
|
398
|
+
let mockResponse: any
|
|
399
|
+
let mockCheckoutConfig: any
|
|
400
|
+
|
|
401
|
+
beforeEach(() => {
|
|
402
|
+
mockNext = vi.fn() as MockedFunction<
|
|
403
|
+
(
|
|
404
|
+
req: UnaryRequest | StreamRequest
|
|
405
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
406
|
+
>
|
|
407
|
+
|
|
408
|
+
mockRequest = {
|
|
409
|
+
header: new Headers(),
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
mockResponse = {
|
|
413
|
+
header: new Headers(),
|
|
414
|
+
message: { $typeName: 'test.Message' },
|
|
415
|
+
trailer: new Headers(),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
mockCheckoutConfig = {
|
|
419
|
+
hostname: 'example.com',
|
|
420
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
421
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
422
|
+
cmsURL: 'https://cms-local.example.com',
|
|
423
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
424
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
425
|
+
forestAPIKey: 'forest-api-key',
|
|
426
|
+
saleChannelAccessKey: 'test-sales-channel-access',
|
|
427
|
+
salesChannelAPISecret: 'test-sales-channel-secret',
|
|
428
|
+
storeAccessKey: 'test-store-access',
|
|
429
|
+
storeAPISecret: 'test-store-secret',
|
|
430
|
+
isProduction: false,
|
|
431
|
+
posthogKey: 'posthog-key',
|
|
432
|
+
posthogDomain: 'posthog.example.com',
|
|
433
|
+
posthogHost: 'posthog.example.com',
|
|
434
|
+
assetsPath: '/assets',
|
|
435
|
+
assetsDomain: 'assets.example.com',
|
|
436
|
+
turnstileKey: 'turnstile-key',
|
|
437
|
+
turnstileSecret: 'turnstile-secret',
|
|
438
|
+
redis: {
|
|
439
|
+
user: 'redis-user',
|
|
440
|
+
host: 'localhost',
|
|
441
|
+
password: 'redis-password',
|
|
442
|
+
port: 6379,
|
|
443
|
+
},
|
|
444
|
+
} as any
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should create an authentication interceptor', () => {
|
|
448
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
449
|
+
expect(typeof interceptor).toBe('function')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should set all authentication headers', async () => {
|
|
453
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
454
|
+
const interceptorFn = interceptor(mockNext)
|
|
455
|
+
|
|
456
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
457
|
+
|
|
458
|
+
const result = await interceptorFn(mockRequest as any)
|
|
459
|
+
|
|
460
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
461
|
+
expect(mockRequest.header.get('X-SA-Key')).toBe('test-store-access')
|
|
462
|
+
expect(mockRequest.header.get('X-CH-Key')).toBe(
|
|
463
|
+
'test-sales-channel-secret'
|
|
464
|
+
)
|
|
465
|
+
expect(mockRequest.header.get('X-CHA-Key')).toBe(
|
|
466
|
+
'test-sales-channel-access'
|
|
467
|
+
)
|
|
468
|
+
expect(mockNext).toHaveBeenCalledWith(mockRequest)
|
|
469
|
+
expect(result).toBe(mockResponse)
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
it('should set headers exactly once per call', async () => {
|
|
473
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
474
|
+
const interceptorFn = interceptor(mockNext)
|
|
475
|
+
|
|
476
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
477
|
+
|
|
478
|
+
await interceptorFn(mockRequest as any)
|
|
479
|
+
|
|
480
|
+
// Verify headers are set exactly once
|
|
481
|
+
const headerSetCalls = mockRequest.header.set.mock?.calls || []
|
|
482
|
+
// Headers.set might not be mockable, so we verify via get
|
|
483
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
484
|
+
expect(mockRequest.header.get('X-SA-Key')).toBe('test-store-access')
|
|
485
|
+
expect(mockRequest.header.get('X-CH-Key')).toBe(
|
|
486
|
+
'test-sales-channel-secret'
|
|
487
|
+
)
|
|
488
|
+
expect(mockRequest.header.get('X-CHA-Key')).toBe(
|
|
489
|
+
'test-sales-channel-access'
|
|
490
|
+
)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
it('should work with stream requests', async () => {
|
|
494
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
495
|
+
const interceptorFn = interceptor(mockNext)
|
|
496
|
+
|
|
497
|
+
const streamRequest = {
|
|
498
|
+
...mockRequest,
|
|
499
|
+
stream: true,
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
503
|
+
|
|
504
|
+
const result = await interceptorFn(streamRequest as any)
|
|
505
|
+
|
|
506
|
+
expect(streamRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
507
|
+
expect(streamRequest.header.get('X-SA-Key')).toBe('test-store-access')
|
|
508
|
+
expect(streamRequest.header.get('X-CH-Key')).toBe(
|
|
509
|
+
'test-sales-channel-secret'
|
|
510
|
+
)
|
|
511
|
+
expect(streamRequest.header.get('X-CHA-Key')).toBe(
|
|
512
|
+
'test-sales-channel-access'
|
|
513
|
+
)
|
|
514
|
+
expect(result).toBe(mockResponse)
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
it('should pass request unchanged to next interceptor', async () => {
|
|
518
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
519
|
+
const interceptorFn = interceptor(mockNext)
|
|
520
|
+
|
|
521
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
522
|
+
|
|
523
|
+
const originalRequest = mockRequest
|
|
524
|
+
await interceptorFn(originalRequest)
|
|
525
|
+
|
|
526
|
+
expect(mockNext).toHaveBeenCalledWith(originalRequest)
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('should handle empty config values', async () => {
|
|
530
|
+
const emptyConfig = {
|
|
531
|
+
hostname: 'example.com',
|
|
532
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
533
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
534
|
+
cmsURL: 'https://cms-local.example.com',
|
|
535
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
536
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
537
|
+
forestAPIKey: 'forest-api-key',
|
|
538
|
+
saleChannelAccessKey: '',
|
|
539
|
+
salesChannelAPISecret: '',
|
|
540
|
+
storeAccessKey: '',
|
|
541
|
+
storeAPISecret: '',
|
|
542
|
+
isProduction: false,
|
|
543
|
+
posthogKey: 'posthog-key',
|
|
544
|
+
posthogDomain: 'posthog.example.com',
|
|
545
|
+
posthogHost: 'posthog.example.com',
|
|
546
|
+
assetsPath: '/assets',
|
|
547
|
+
assetsDomain: 'assets.example.com',
|
|
548
|
+
turnstileKey: 'turnstile-key',
|
|
549
|
+
turnstileSecret: 'turnstile-secret',
|
|
550
|
+
redis: {
|
|
551
|
+
user: 'redis-user',
|
|
552
|
+
host: 'localhost',
|
|
553
|
+
password: 'redis-password',
|
|
554
|
+
port: 6379,
|
|
555
|
+
},
|
|
556
|
+
} as any
|
|
557
|
+
|
|
558
|
+
const interceptor = createAuthenticationInterceptor(emptyConfig)
|
|
559
|
+
const interceptorFn = interceptor(mockNext)
|
|
560
|
+
|
|
561
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
562
|
+
|
|
563
|
+
const result = await interceptorFn(mockRequest as any)
|
|
564
|
+
|
|
565
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('')
|
|
566
|
+
expect(mockRequest.header.get('X-SA-Key')).toBe('')
|
|
567
|
+
expect(result).toBe(mockResponse)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it('should propagate response from next interceptor', async () => {
|
|
571
|
+
const interceptor = createAuthenticationInterceptor(mockCheckoutConfig)
|
|
572
|
+
const interceptorFn = interceptor(mockNext)
|
|
573
|
+
|
|
574
|
+
const customResponse = {
|
|
575
|
+
...mockResponse,
|
|
576
|
+
message: { $typeName: 'custom.Message', data: 'custom' },
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
mockNext.mockResolvedValue(customResponse)
|
|
580
|
+
|
|
581
|
+
const result = await interceptor(mockNext)(mockRequest as any)
|
|
582
|
+
|
|
583
|
+
expect(result).toBe(customResponse)
|
|
584
|
+
expect((result.message as any).data).toBe('custom')
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe('createCustomerAuthenticationInterceptor', () => {
|
|
589
|
+
let mockNext: MockedFunction<
|
|
590
|
+
(
|
|
591
|
+
req: UnaryRequest | StreamRequest
|
|
592
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
593
|
+
>
|
|
594
|
+
let mockRequest: any
|
|
595
|
+
let mockResponse: any
|
|
596
|
+
|
|
597
|
+
beforeEach(() => {
|
|
598
|
+
mockNext = vi.fn() as MockedFunction<
|
|
599
|
+
(
|
|
600
|
+
req: UnaryRequest | StreamRequest
|
|
601
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
602
|
+
>
|
|
603
|
+
|
|
604
|
+
mockRequest = {
|
|
605
|
+
header: new Headers(),
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
mockResponse = {
|
|
609
|
+
header: new Headers(),
|
|
610
|
+
message: { $typeName: 'test.Message' },
|
|
611
|
+
trailer: new Headers(),
|
|
612
|
+
}
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('should create a customer authentication interceptor', () => {
|
|
616
|
+
const interceptor =
|
|
617
|
+
createCustomerAuthenticationInterceptor('test-jwt-token')
|
|
618
|
+
expect(typeof interceptor).toBe('function')
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('should set Authorization header with Bearer token', async () => {
|
|
622
|
+
const jwtToken = 'test-jwt-token'
|
|
623
|
+
const interceptor = createCustomerAuthenticationInterceptor(jwtToken)
|
|
624
|
+
const interceptorFn = interceptor(mockNext)
|
|
625
|
+
|
|
626
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
627
|
+
|
|
628
|
+
const result = await interceptorFn(mockRequest as any)
|
|
629
|
+
|
|
630
|
+
expect(mockRequest.header.get('Authorization')).toBe(`Bearer ${jwtToken}`)
|
|
631
|
+
expect(mockNext).toHaveBeenCalledWith(mockRequest)
|
|
632
|
+
expect(result).toBe(mockResponse)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('should work with various JWT token formats', async () => {
|
|
636
|
+
const tokens = [
|
|
637
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
|
|
638
|
+
'simple-token',
|
|
639
|
+
'token-with-many-dashes-123-456-789',
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
for (const token of tokens) {
|
|
643
|
+
const interceptor = createCustomerAuthenticationInterceptor(token)
|
|
644
|
+
const interceptorFn = interceptor(mockNext)
|
|
645
|
+
|
|
646
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
647
|
+
|
|
648
|
+
const request = { header: new Headers() }
|
|
649
|
+
await interceptorFn(request as any)
|
|
650
|
+
|
|
651
|
+
expect(request.header.get('Authorization')).toBe(`Bearer ${token}`)
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
it('should work with stream requests', async () => {
|
|
656
|
+
const jwtToken = 'test-jwt-token'
|
|
657
|
+
const interceptor = createCustomerAuthenticationInterceptor(jwtToken)
|
|
658
|
+
const interceptorFn = interceptor(mockNext)
|
|
659
|
+
|
|
660
|
+
const streamRequest = {
|
|
661
|
+
...mockRequest,
|
|
662
|
+
stream: true,
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
666
|
+
|
|
667
|
+
const result = await interceptorFn(streamRequest as any)
|
|
668
|
+
|
|
669
|
+
expect(streamRequest.header.get('Authorization')).toBe(
|
|
670
|
+
`Bearer ${jwtToken}`
|
|
671
|
+
)
|
|
672
|
+
expect(result).toBe(mockResponse)
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('should throw error for empty JWT token', async () => {
|
|
676
|
+
const interceptor = createCustomerAuthenticationInterceptor('')
|
|
677
|
+
const interceptorFn = interceptor(mockNext)
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
await interceptorFn(mockRequest as any)
|
|
681
|
+
expect.fail('Should have thrown error')
|
|
682
|
+
} catch (error) {
|
|
683
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
684
|
+
}
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
it('should throw Unauthenticated error code for empty JWT', async () => {
|
|
688
|
+
const interceptor = createCustomerAuthenticationInterceptor('')
|
|
689
|
+
const interceptorFn = interceptor(mockNext)
|
|
690
|
+
|
|
691
|
+
try {
|
|
692
|
+
await interceptorFn(mockRequest as any)
|
|
693
|
+
expect.fail('Should have thrown error')
|
|
694
|
+
} catch (error) {
|
|
695
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
696
|
+
expect((error as ConnectError).code).toBe(Code.Unauthenticated)
|
|
697
|
+
}
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('should throw error for null JWT token', async () => {
|
|
701
|
+
const interceptor = createCustomerAuthenticationInterceptor(null as any)
|
|
702
|
+
const interceptorFn = interceptor(mockNext)
|
|
703
|
+
|
|
704
|
+
await expect(interceptorFn(mockRequest as any)).rejects.toThrow()
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it('should throw error for undefined JWT token', async () => {
|
|
708
|
+
const interceptor = createCustomerAuthenticationInterceptor(
|
|
709
|
+
undefined as any
|
|
710
|
+
)
|
|
711
|
+
const interceptorFn = interceptor(mockNext)
|
|
712
|
+
|
|
713
|
+
await expect(interceptorFn(mockRequest as any)).rejects.toThrow()
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('should not call next when JWT validation fails', async () => {
|
|
717
|
+
const interceptor = createCustomerAuthenticationInterceptor('')
|
|
718
|
+
const interceptorFn = interceptor(mockNext)
|
|
719
|
+
|
|
720
|
+
await expect(interceptorFn(mockRequest as any)).rejects.toThrow()
|
|
721
|
+
|
|
722
|
+
// next should never be called if validation fails
|
|
723
|
+
expect(mockNext).not.toHaveBeenCalled()
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
it('should propagate response from next interceptor', async () => {
|
|
727
|
+
const jwtToken = 'test-jwt-token'
|
|
728
|
+
const interceptor = createCustomerAuthenticationInterceptor(jwtToken)
|
|
729
|
+
const interceptorFn = interceptor(mockNext)
|
|
730
|
+
|
|
731
|
+
const customResponse = {
|
|
732
|
+
...mockResponse,
|
|
733
|
+
message: { $typeName: 'custom.Message', userId: 'user123' },
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
mockNext.mockResolvedValue(customResponse)
|
|
737
|
+
|
|
738
|
+
const result = await interceptorFn(mockRequest)
|
|
739
|
+
|
|
740
|
+
expect(result).toBe(customResponse)
|
|
741
|
+
expect((result.message as any).userId).toBe('user123')
|
|
742
|
+
})
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
describe('createHeadersInterceptor', () => {
|
|
746
|
+
let mockNext: MockedFunction<
|
|
747
|
+
(
|
|
748
|
+
req: UnaryRequest | StreamRequest
|
|
749
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
750
|
+
>
|
|
751
|
+
let mockRequest: any
|
|
752
|
+
let mockResponse: any
|
|
753
|
+
|
|
754
|
+
beforeEach(() => {
|
|
755
|
+
mockNext = vi.fn() as MockedFunction<
|
|
756
|
+
(
|
|
757
|
+
req: UnaryRequest | StreamRequest
|
|
758
|
+
) => Promise<UnaryResponse | StreamResponse>
|
|
759
|
+
>
|
|
760
|
+
|
|
761
|
+
mockRequest = {
|
|
762
|
+
header: new Headers(),
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
mockResponse = {
|
|
766
|
+
header: new Headers(),
|
|
767
|
+
message: { $typeName: 'test.Message' },
|
|
768
|
+
trailer: new Headers(),
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
it('should set expected headers from header mapping', async () => {
|
|
773
|
+
const customHeaders = {
|
|
774
|
+
'x-original-host': 'example.com',
|
|
775
|
+
'x-shop-id': 'shop123',
|
|
776
|
+
'x-sitepath': '/path',
|
|
777
|
+
'x-locale': 'en',
|
|
778
|
+
'x-region': 'US',
|
|
779
|
+
}
|
|
780
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
781
|
+
const interceptorFn = interceptor(mockNext)
|
|
782
|
+
|
|
783
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
784
|
+
|
|
785
|
+
const result = await interceptorFn(mockRequest as any)
|
|
786
|
+
|
|
787
|
+
expect(mockRequest.header.get('X-Original-Host')).toBe('example.com')
|
|
788
|
+
expect(mockRequest.header.get('X-shop-id')).toBe('shop123')
|
|
789
|
+
expect(mockRequest.header.get('X-Sitepath')).toBe('/path')
|
|
790
|
+
expect(mockRequest.header.get('X-locale')).toBe('en')
|
|
791
|
+
expect(mockRequest.header.get('X-region')).toBe('US')
|
|
792
|
+
|
|
793
|
+
expect(mockNext).toHaveBeenCalledWith(mockRequest)
|
|
794
|
+
expect(result).toBe(mockResponse)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('should throw error when x-original-host is missing', async () => {
|
|
798
|
+
const customHeaders = {
|
|
799
|
+
'x-shop-id': 'shop123',
|
|
800
|
+
'x-sitepath': '/path',
|
|
801
|
+
'x-locale': 'en',
|
|
802
|
+
'x-region': 'US',
|
|
803
|
+
}
|
|
804
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
805
|
+
const interceptorFn = interceptor(mockNext)
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
await interceptorFn(mockRequest as any)
|
|
809
|
+
expect.fail('Should have thrown error')
|
|
810
|
+
} catch (error) {
|
|
811
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
812
|
+
expect((error as ConnectError).code).toBe(Code.InvalidArgument)
|
|
813
|
+
expect((error as ConnectError).message).toContain('Original host')
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
it('should throw error when x-shop-id is missing', async () => {
|
|
818
|
+
const customHeaders = {
|
|
819
|
+
'x-original-host': 'example.com',
|
|
820
|
+
'x-sitepath': '/path',
|
|
821
|
+
'x-locale': 'en',
|
|
822
|
+
'x-region': 'US',
|
|
823
|
+
}
|
|
824
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
825
|
+
const interceptorFn = interceptor(mockNext)
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
await interceptorFn(mockRequest as any)
|
|
829
|
+
expect.fail('Should have thrown error')
|
|
830
|
+
} catch (error) {
|
|
831
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
832
|
+
expect((error as ConnectError).message).toContain('Shop id')
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
it('should throw error when x-sitepath is missing', async () => {
|
|
837
|
+
const customHeaders = {
|
|
838
|
+
'x-original-host': 'example.com',
|
|
839
|
+
'x-shop-id': 'shop123',
|
|
840
|
+
'x-locale': 'en',
|
|
841
|
+
'x-region': 'US',
|
|
842
|
+
}
|
|
843
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
844
|
+
const interceptorFn = interceptor(mockNext)
|
|
845
|
+
|
|
846
|
+
try {
|
|
847
|
+
await interceptorFn(mockRequest as any)
|
|
848
|
+
expect.fail('Should have thrown error')
|
|
849
|
+
} catch (error) {
|
|
850
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
851
|
+
expect((error as ConnectError).message).toContain('Sitepath')
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('should throw error when x-locale is missing', async () => {
|
|
856
|
+
const customHeaders = {
|
|
857
|
+
'x-original-host': 'example.com',
|
|
858
|
+
'x-shop-id': 'shop123',
|
|
859
|
+
'x-sitepath': '/path',
|
|
860
|
+
'x-region': 'US',
|
|
861
|
+
}
|
|
862
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
863
|
+
const interceptorFn = interceptor(mockNext)
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
await interceptorFn(mockRequest as any)
|
|
867
|
+
expect.fail('Should have thrown error')
|
|
868
|
+
} catch (error) {
|
|
869
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
870
|
+
expect((error as ConnectError).message).toContain('Locale')
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it('should throw error when x-region is missing', async () => {
|
|
875
|
+
const customHeaders = {
|
|
876
|
+
'x-original-host': 'example.com',
|
|
877
|
+
'x-shop-id': 'shop123',
|
|
878
|
+
'x-sitepath': '/path',
|
|
879
|
+
'x-locale': 'en',
|
|
880
|
+
}
|
|
881
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
882
|
+
const interceptorFn = interceptor(mockNext)
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
await interceptorFn(mockRequest as any)
|
|
886
|
+
expect.fail('Should have thrown error')
|
|
887
|
+
} catch (error) {
|
|
888
|
+
expect(error).toBeInstanceOf(ConnectError)
|
|
889
|
+
expect((error as ConnectError).message).toContain('Region')
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
it('should throw error when x-original-host is null', async () => {
|
|
894
|
+
const customHeaders = {
|
|
895
|
+
'x-original-host': null,
|
|
896
|
+
'x-shop-id': 'shop123',
|
|
897
|
+
'x-sitepath': '/path',
|
|
898
|
+
'x-locale': 'en',
|
|
899
|
+
'x-region': 'US',
|
|
900
|
+
}
|
|
901
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
902
|
+
const interceptorFn = interceptor(mockNext)
|
|
903
|
+
|
|
904
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
905
|
+
})
|
|
906
|
+
|
|
907
|
+
it('should throw error when x-shop-id is null', async () => {
|
|
908
|
+
const customHeaders = {
|
|
909
|
+
'x-original-host': 'example.com',
|
|
910
|
+
'x-shop-id': null,
|
|
911
|
+
'x-sitepath': '/path',
|
|
912
|
+
'x-locale': 'en',
|
|
913
|
+
'x-region': 'US',
|
|
914
|
+
}
|
|
915
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
916
|
+
const interceptorFn = interceptor(mockNext)
|
|
917
|
+
|
|
918
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
it('should throw error when x-sitepath is null', async () => {
|
|
922
|
+
const customHeaders = {
|
|
923
|
+
'x-original-host': 'example.com',
|
|
924
|
+
'x-shop-id': 'shop123',
|
|
925
|
+
'x-sitepath': null,
|
|
926
|
+
'x-locale': 'en',
|
|
927
|
+
'x-region': 'US',
|
|
928
|
+
}
|
|
929
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
930
|
+
const interceptorFn = interceptor(mockNext)
|
|
931
|
+
|
|
932
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('should throw error when x-locale is null', async () => {
|
|
936
|
+
const customHeaders = {
|
|
937
|
+
'x-original-host': 'example.com',
|
|
938
|
+
'x-shop-id': 'shop123',
|
|
939
|
+
'x-sitepath': '/path',
|
|
940
|
+
'x-locale': null,
|
|
941
|
+
'x-region': 'US',
|
|
942
|
+
}
|
|
943
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
944
|
+
const interceptorFn = interceptor(mockNext)
|
|
945
|
+
|
|
946
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
it('should throw error when x-region is null', async () => {
|
|
950
|
+
const customHeaders = {
|
|
951
|
+
'x-original-host': 'example.com',
|
|
952
|
+
'x-shop-id': 'shop123',
|
|
953
|
+
'x-sitepath': '/path',
|
|
954
|
+
'x-locale': 'en',
|
|
955
|
+
'x-region': null,
|
|
956
|
+
}
|
|
957
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
958
|
+
const interceptorFn = interceptor(mockNext)
|
|
959
|
+
|
|
960
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
it('should work with stream requests', async () => {
|
|
964
|
+
const customHeaders = {
|
|
965
|
+
'x-original-host': 'example.com',
|
|
966
|
+
'x-shop-id': 'shop123',
|
|
967
|
+
'x-sitepath': '/path',
|
|
968
|
+
'x-locale': 'en',
|
|
969
|
+
'x-region': 'US',
|
|
970
|
+
}
|
|
971
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
972
|
+
const interceptorFn = interceptor(mockNext)
|
|
973
|
+
|
|
974
|
+
const streamRequest = {
|
|
975
|
+
header: new Headers(),
|
|
976
|
+
stream: true,
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
980
|
+
|
|
981
|
+
const result = await interceptorFn(streamRequest as any)
|
|
982
|
+
|
|
983
|
+
expect(streamRequest.header.get('X-Original-Host')).toBe('example.com')
|
|
984
|
+
expect(streamRequest.header.get('X-shop-id')).toBe('shop123')
|
|
985
|
+
expect(result).toBe(mockResponse)
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
it('should not call next when header validation fails', async () => {
|
|
989
|
+
const customHeaders = {
|
|
990
|
+
'x-original-host': 'example.com',
|
|
991
|
+
// Missing x-shop-id
|
|
992
|
+
'x-sitepath': '/path',
|
|
993
|
+
'x-locale': 'en',
|
|
994
|
+
'x-region': 'US',
|
|
995
|
+
}
|
|
996
|
+
const interceptor = createHeadersInterceptor(customHeaders as any)
|
|
997
|
+
const interceptorFn = interceptor(mockNext)
|
|
998
|
+
|
|
999
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
1000
|
+
|
|
1001
|
+
// next should never be called if validation fails
|
|
1002
|
+
expect(mockNext).not.toHaveBeenCalled()
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
it('should propagate response from next interceptor', async () => {
|
|
1006
|
+
const customHeaders = {
|
|
1007
|
+
'x-original-host': 'example.com',
|
|
1008
|
+
'x-shop-id': 'shop123',
|
|
1009
|
+
'x-sitepath': '/path',
|
|
1010
|
+
'x-locale': 'en',
|
|
1011
|
+
'x-region': 'US',
|
|
1012
|
+
}
|
|
1013
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
1014
|
+
const interceptorFn = interceptor(mockNext)
|
|
1015
|
+
|
|
1016
|
+
const customResponse = {
|
|
1017
|
+
...mockResponse,
|
|
1018
|
+
message: { $typeName: 'custom.Message', requestId: 'req123' },
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
mockNext.mockResolvedValue(customResponse)
|
|
1022
|
+
|
|
1023
|
+
const result = await interceptor(mockNext)(mockRequest as any)
|
|
1024
|
+
|
|
1025
|
+
expect(result).toBe(customResponse)
|
|
1026
|
+
expect((result.message as any).requestId).toBe('req123')
|
|
1027
|
+
})
|
|
1028
|
+
|
|
1029
|
+
it('should handle headers with special characters', async () => {
|
|
1030
|
+
const customHeaders = {
|
|
1031
|
+
'x-original-host': 'example.com:8080',
|
|
1032
|
+
'x-shop-id': 'shop-123-456',
|
|
1033
|
+
'x-sitepath': '/en-GB/products',
|
|
1034
|
+
'x-locale': 'en_GB',
|
|
1035
|
+
'x-region': 'EU-West-1',
|
|
1036
|
+
}
|
|
1037
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
1038
|
+
const interceptorFn = interceptor(mockNext)
|
|
1039
|
+
|
|
1040
|
+
mockNext.mockResolvedValue(mockResponse)
|
|
1041
|
+
|
|
1042
|
+
const result = await interceptorFn(mockRequest)
|
|
1043
|
+
|
|
1044
|
+
expect(mockRequest.header.get('X-Original-Host')).toBe('example.com:8080')
|
|
1045
|
+
expect(mockRequest.header.get('X-shop-id')).toBe('shop-123-456')
|
|
1046
|
+
expect(mockRequest.header.get('X-Sitepath')).toBe('/en-GB/products')
|
|
1047
|
+
expect(result).toBe(mockResponse)
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
it('should handle empty string headers (should fail validation)', async () => {
|
|
1051
|
+
const customHeaders = {
|
|
1052
|
+
'x-original-host': '',
|
|
1053
|
+
'x-shop-id': 'shop123',
|
|
1054
|
+
'x-sitepath': '/path',
|
|
1055
|
+
'x-locale': 'en',
|
|
1056
|
+
'x-region': 'US',
|
|
1057
|
+
}
|
|
1058
|
+
const interceptor = createHeadersInterceptor(customHeaders)
|
|
1059
|
+
const interceptorFn = interceptor(mockNext)
|
|
1060
|
+
|
|
1061
|
+
// Empty string is falsy, so validation should fail
|
|
1062
|
+
await expect(interceptorFn(mockRequest)).rejects.toThrow()
|
|
1063
|
+
})
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
describe('interceptor integration', () => {
|
|
1067
|
+
it('should chain multiple interceptors in order', async () => {
|
|
1068
|
+
const jwtToken = 'test-jwt'
|
|
1069
|
+
const mockNext = vi.fn() as any
|
|
1070
|
+
|
|
1071
|
+
mockNext.mockResolvedValue({
|
|
1072
|
+
header: new Headers(),
|
|
1073
|
+
message: { $typeName: 'test.Message' },
|
|
1074
|
+
trailer: new Headers(),
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
const mockCheckoutConfig = {
|
|
1078
|
+
hostname: 'example.com',
|
|
1079
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
1080
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
1081
|
+
cmsURL: 'https://cms-local.example.com',
|
|
1082
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
1083
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
1084
|
+
forestAPIKey: 'forest-api-key',
|
|
1085
|
+
saleChannelAccessKey: 'test-sales-channel-access',
|
|
1086
|
+
salesChannelAPISecret: 'test-sales-channel-secret',
|
|
1087
|
+
storeAccessKey: 'test-store-access',
|
|
1088
|
+
storeAPISecret: 'test-store-secret',
|
|
1089
|
+
isProduction: false,
|
|
1090
|
+
posthogKey: 'posthog-key',
|
|
1091
|
+
posthogDomain: 'posthog.example.com',
|
|
1092
|
+
posthogHost: 'posthog.example.com',
|
|
1093
|
+
assetsPath: '/assets',
|
|
1094
|
+
assetsDomain: 'assets.example.com',
|
|
1095
|
+
turnstileKey: 'turnstile-key',
|
|
1096
|
+
turnstileSecret: 'turnstile-secret',
|
|
1097
|
+
redis: {
|
|
1098
|
+
user: 'redis-user',
|
|
1099
|
+
host: 'localhost',
|
|
1100
|
+
password: 'redis-password',
|
|
1101
|
+
port: 6379,
|
|
1102
|
+
},
|
|
1103
|
+
} as any
|
|
1104
|
+
|
|
1105
|
+
// Create interceptors
|
|
1106
|
+
const authInterceptor = createAuthenticationInterceptor(
|
|
1107
|
+
mockCheckoutConfig as any
|
|
1108
|
+
)
|
|
1109
|
+
const customerAuthInterceptor =
|
|
1110
|
+
createCustomerAuthenticationInterceptor(jwtToken)
|
|
1111
|
+
const loggingInterceptor = createLoggingInterceptor()
|
|
1112
|
+
|
|
1113
|
+
// Chain them together
|
|
1114
|
+
const chainedInterceptor = authInterceptor(
|
|
1115
|
+
customerAuthInterceptor(loggingInterceptor(mockNext))
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
const mockRequest = {
|
|
1119
|
+
header: new Headers(),
|
|
1120
|
+
method: {
|
|
1121
|
+
toString: () => 'TestService/TestMethod',
|
|
1122
|
+
},
|
|
1123
|
+
message: { $typeName: 'test.Message' },
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
await chainedInterceptor(mockRequest as any)
|
|
1127
|
+
|
|
1128
|
+
// Verify all headers are set
|
|
1129
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
1130
|
+
expect(mockRequest.header.get('X-SA-Key')).toBe('test-store-access')
|
|
1131
|
+
expect(mockRequest.header.get('Authorization')).toBe(`Bearer ${jwtToken}`)
|
|
1132
|
+
expect(mockNext).toHaveBeenCalled()
|
|
1133
|
+
})
|
|
1134
|
+
|
|
1135
|
+
it('should handle errors in chained interceptors', async () => {
|
|
1136
|
+
const jwtToken = ''
|
|
1137
|
+
|
|
1138
|
+
const mockNext = vi.fn() as any
|
|
1139
|
+
mockNext.mockResolvedValue({
|
|
1140
|
+
header: new Headers(),
|
|
1141
|
+
message: { $typeName: 'test.Message' },
|
|
1142
|
+
trailer: new Headers(),
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
const mockCheckoutConfig = {
|
|
1146
|
+
hostname: 'example.com',
|
|
1147
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
1148
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
1149
|
+
cmsURL: 'https://cms-local.example.com',
|
|
1150
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
1151
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
1152
|
+
forestAPIKey: 'forest-api-key',
|
|
1153
|
+
saleChannelAccessKey: 'test-sales-channel-access',
|
|
1154
|
+
salesChannelAPISecret: 'test-sales-channel-secret',
|
|
1155
|
+
storeAccessKey: 'test-store-access',
|
|
1156
|
+
storeAPISecret: 'test-store-secret',
|
|
1157
|
+
isProduction: false,
|
|
1158
|
+
posthogKey: 'posthog-key',
|
|
1159
|
+
posthogDomain: 'posthog.example.com',
|
|
1160
|
+
posthogHost: 'posthog.example.com',
|
|
1161
|
+
assetsPath: '/assets',
|
|
1162
|
+
assetsDomain: 'assets.example.com',
|
|
1163
|
+
turnstileKey: 'turnstile-key',
|
|
1164
|
+
turnstileSecret: 'turnstile-secret',
|
|
1165
|
+
redis: {
|
|
1166
|
+
user: 'redis-user',
|
|
1167
|
+
host: 'localhost',
|
|
1168
|
+
password: 'redis-password',
|
|
1169
|
+
port: 6379,
|
|
1170
|
+
},
|
|
1171
|
+
} as any
|
|
1172
|
+
|
|
1173
|
+
// Chain: auth -> customer auth (will fail) -> logging
|
|
1174
|
+
const authInterceptor = createAuthenticationInterceptor(
|
|
1175
|
+
mockCheckoutConfig as any
|
|
1176
|
+
)
|
|
1177
|
+
const customerAuthInterceptor =
|
|
1178
|
+
createCustomerAuthenticationInterceptor(jwtToken)
|
|
1179
|
+
const loggingInterceptor = createLoggingInterceptor()
|
|
1180
|
+
|
|
1181
|
+
const chainedInterceptor = authInterceptor(
|
|
1182
|
+
customerAuthInterceptor(loggingInterceptor(mockNext))
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
const mockRequest = {
|
|
1186
|
+
header: new Headers(),
|
|
1187
|
+
method: {
|
|
1188
|
+
toString: () => 'TestService/TestMethod',
|
|
1189
|
+
},
|
|
1190
|
+
message: { $typeName: 'test.Message' },
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Should fail at customer auth interceptor
|
|
1194
|
+
await expect(chainedInterceptor(mockRequest as any)).rejects.toThrow()
|
|
1195
|
+
|
|
1196
|
+
// Auth headers should still be set before error
|
|
1197
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
1198
|
+
|
|
1199
|
+
// But next should not be called
|
|
1200
|
+
expect(mockNext).not.toHaveBeenCalled()
|
|
1201
|
+
})
|
|
1202
|
+
|
|
1203
|
+
it('should work with all interceptors including headers', async () => {
|
|
1204
|
+
const jwtToken = 'test-jwt'
|
|
1205
|
+
const mockNext = vi.fn() as any
|
|
1206
|
+
|
|
1207
|
+
mockNext.mockResolvedValue({
|
|
1208
|
+
header: new Headers(),
|
|
1209
|
+
message: { $typeName: 'test.Message' },
|
|
1210
|
+
trailer: new Headers(),
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
const mockCheckoutConfig = {
|
|
1214
|
+
hostname: 'example.com',
|
|
1215
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
1216
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
1217
|
+
cmsURL: 'https://cms-local.example.com',
|
|
1218
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
1219
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
1220
|
+
forestAPIKey: 'forest-api-key',
|
|
1221
|
+
saleChannelAccessKey: 'test-sales-channel-access',
|
|
1222
|
+
salesChannelAPISecret: 'test-sales-channel-secret',
|
|
1223
|
+
storeAccessKey: 'test-store-access',
|
|
1224
|
+
storeAPISecret: 'test-store-secret',
|
|
1225
|
+
isProduction: false,
|
|
1226
|
+
posthogKey: 'posthog-key',
|
|
1227
|
+
posthogDomain: 'posthog.example.com',
|
|
1228
|
+
posthogHost: 'posthog.example.com',
|
|
1229
|
+
assetsPath: '/assets',
|
|
1230
|
+
assetsDomain: 'assets.example.com',
|
|
1231
|
+
turnstileKey: 'turnstile-key',
|
|
1232
|
+
turnstileSecret: 'turnstile-secret',
|
|
1233
|
+
redis: {
|
|
1234
|
+
user: 'redis-user',
|
|
1235
|
+
host: 'localhost',
|
|
1236
|
+
password: 'redis-password',
|
|
1237
|
+
port: 6379,
|
|
1238
|
+
},
|
|
1239
|
+
} as any
|
|
1240
|
+
|
|
1241
|
+
const customHeaders = {
|
|
1242
|
+
'x-original-host': 'example.com',
|
|
1243
|
+
'x-shop-id': 'shop123',
|
|
1244
|
+
'x-sitepath': '/path',
|
|
1245
|
+
'x-locale': 'en',
|
|
1246
|
+
'x-region': 'US',
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
// Chain all interceptors
|
|
1250
|
+
const headersInterceptor = createHeadersInterceptor(customHeaders)
|
|
1251
|
+
const authInterceptor = createAuthenticationInterceptor(
|
|
1252
|
+
mockCheckoutConfig as any
|
|
1253
|
+
)
|
|
1254
|
+
const customerAuthInterceptor =
|
|
1255
|
+
createCustomerAuthenticationInterceptor(jwtToken)
|
|
1256
|
+
const loggingInterceptor = createLoggingInterceptor()
|
|
1257
|
+
|
|
1258
|
+
const chainedInterceptor = headersInterceptor(
|
|
1259
|
+
authInterceptor(customerAuthInterceptor(loggingInterceptor(mockNext)))
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
const mockRequest = {
|
|
1263
|
+
header: new Headers(),
|
|
1264
|
+
method: {
|
|
1265
|
+
toString: () => 'TestService/TestMethod',
|
|
1266
|
+
},
|
|
1267
|
+
message: { $typeName: 'test.Message' },
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
await chainedInterceptor(mockRequest as any)
|
|
1271
|
+
|
|
1272
|
+
// Verify all headers from all interceptors are set
|
|
1273
|
+
expect(mockRequest.header.get('X-Original-Host')).toBe('example.com')
|
|
1274
|
+
expect(mockRequest.header.get('X-shop-id')).toBe('shop123')
|
|
1275
|
+
expect(mockRequest.header.get('X-Store-Key')).toBe('test-store-secret')
|
|
1276
|
+
expect(mockRequest.header.get('Authorization')).toBe(`Bearer ${jwtToken}`)
|
|
1277
|
+
expect(mockNext).toHaveBeenCalled()
|
|
1278
|
+
})
|
|
1279
|
+
|
|
1280
|
+
it('should validate headers before auth in chain', async () => {
|
|
1281
|
+
const mockNext = vi.fn() as any
|
|
1282
|
+
mockNext.mockResolvedValue({
|
|
1283
|
+
header: new Headers(),
|
|
1284
|
+
message: { $typeName: 'test.Message' },
|
|
1285
|
+
trailer: new Headers(),
|
|
1286
|
+
})
|
|
1287
|
+
|
|
1288
|
+
const mockCheckoutConfig = {
|
|
1289
|
+
hostname: 'example.com',
|
|
1290
|
+
cmsRemoteURL: 'https://cms.example.com',
|
|
1291
|
+
enterpriseRemoteURL: 'https://enterprise.example.com',
|
|
1292
|
+
cmsURL: 'https://cms-local.example.com',
|
|
1293
|
+
enterpriseURL: 'https://enterprise.example.com',
|
|
1294
|
+
secureCookiePassword: 'secure-cookie-password',
|
|
1295
|
+
forestAPIKey: 'forest-api-key',
|
|
1296
|
+
saleChannelAccessKey: 'test-sales-channel-access',
|
|
1297
|
+
salesChannelAPISecret: 'test-sales-channel-secret',
|
|
1298
|
+
storeAccessKey: 'test-store-access',
|
|
1299
|
+
storeAPISecret: 'test-store-secret',
|
|
1300
|
+
isProduction: false,
|
|
1301
|
+
posthogKey: 'posthog-key',
|
|
1302
|
+
posthogDomain: 'posthog.example.com',
|
|
1303
|
+
posthogHost: 'posthog.example.com',
|
|
1304
|
+
assetsPath: '/assets',
|
|
1305
|
+
assetsDomain: 'assets.example.com',
|
|
1306
|
+
turnstileKey: 'turnstile-key',
|
|
1307
|
+
turnstileSecret: 'turnstile-secret',
|
|
1308
|
+
redis: {
|
|
1309
|
+
user: 'redis-user',
|
|
1310
|
+
host: 'localhost',
|
|
1311
|
+
password: 'redis-password',
|
|
1312
|
+
port: 6379,
|
|
1313
|
+
},
|
|
1314
|
+
} as any
|
|
1315
|
+
|
|
1316
|
+
const invalidHeaders = {
|
|
1317
|
+
// Missing x-shop-id
|
|
1318
|
+
'x-original-host': 'example.com',
|
|
1319
|
+
'x-sitepath': '/path',
|
|
1320
|
+
'x-locale': 'en',
|
|
1321
|
+
'x-region': 'US',
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const headersInterceptor = createHeadersInterceptor(invalidHeaders as any)
|
|
1325
|
+
const authInterceptor = createAuthenticationInterceptor(
|
|
1326
|
+
mockCheckoutConfig as any
|
|
1327
|
+
)
|
|
1328
|
+
|
|
1329
|
+
const chainedInterceptor = headersInterceptor(authInterceptor(mockNext))
|
|
1330
|
+
|
|
1331
|
+
const mockRequest = {
|
|
1332
|
+
header: new Headers(),
|
|
1333
|
+
message: { $typeName: 'test.Message' },
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Should fail at headers validation
|
|
1337
|
+
await expect(chainedInterceptor(mockRequest as any)).rejects.toThrow()
|
|
1338
|
+
|
|
1339
|
+
// Auth headers should not be set because headers validation failed
|
|
1340
|
+
expect(mockRequest.header.get('X-Store-Key')).toBeNull()
|
|
1341
|
+
})
|
|
1342
|
+
})
|
|
1343
|
+
})
|