@paakd/api 0.0.1 → 0.0.2

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.
Files changed (201) hide show
  1. package/buf/validate/validate_pb.ts +5001 -0
  2. package/dist/gen/buf/validate/validate_pb.d.ts +4634 -0
  3. package/dist/gen/buf/validate/validate_pb.d.ts.map +1 -0
  4. package/dist/gen/buf/validate/validate_pb.js +414 -0
  5. package/dist/gen/src/proto/auth/v1/entities/auth_pb.d.ts +268 -0
  6. package/dist/gen/src/proto/auth/v1/entities/auth_pb.d.ts.map +1 -0
  7. package/dist/gen/src/proto/auth/v1/entities/auth_pb.js +120 -0
  8. package/dist/gen/src/proto/auth/v1/entities/policy_pb.d.ts +235 -0
  9. package/dist/gen/src/proto/auth/v1/entities/policy_pb.d.ts.map +1 -0
  10. package/dist/gen/src/proto/auth/v1/entities/policy_pb.js +98 -0
  11. package/dist/gen/src/proto/auth/v1/service_pb.d.ts +117 -0
  12. package/dist/gen/src/proto/auth/v1/service_pb.d.ts.map +1 -0
  13. package/dist/gen/src/proto/auth/v1/service_pb.js +22 -0
  14. package/dist/gen/src/proto/customers/v1/entities/address_pb.d.ts +371 -0
  15. package/dist/gen/src/proto/customers/v1/entities/address_pb.d.ts.map +1 -0
  16. package/dist/gen/src/proto/customers/v1/entities/address_pb.js +84 -0
  17. package/dist/gen/src/proto/customers/v1/entities/profile_pb.d.ts +189 -0
  18. package/dist/gen/src/proto/customers/v1/entities/profile_pb.d.ts.map +1 -0
  19. package/dist/gen/src/proto/customers/v1/entities/profile_pb.js +70 -0
  20. package/dist/gen/src/proto/customers/v1/service_pb.d.ts +85 -0
  21. package/dist/gen/src/proto/customers/v1/service_pb.d.ts.map +1 -0
  22. package/dist/gen/src/proto/customers/v1/service_pb.js +22 -0
  23. package/dist/gen/src/proto/products/v1/entities/category_pb.d.ts +234 -0
  24. package/dist/gen/src/proto/products/v1/entities/category_pb.d.ts.map +1 -0
  25. package/dist/gen/src/proto/products/v1/entities/category_pb.js +84 -0
  26. package/dist/gen/src/proto/products/v1/entities/collection_pb.d.ts +159 -0
  27. package/dist/gen/src/proto/products/v1/entities/collection_pb.d.ts.map +1 -0
  28. package/dist/gen/src/proto/products/v1/entities/collection_pb.js +70 -0
  29. package/dist/gen/src/proto/products/v1/entities/product/create_pb.d.ts +146 -0
  30. package/dist/gen/src/proto/products/v1/entities/product/create_pb.d.ts.map +1 -0
  31. package/dist/gen/src/proto/products/v1/entities/product/create_pb.js +32 -0
  32. package/dist/gen/src/proto/products/v1/entities/product/option_pb.d.ts +50 -0
  33. package/dist/gen/src/proto/products/v1/entities/product/option_pb.d.ts.map +1 -0
  34. package/dist/gen/src/proto/products/v1/entities/product/option_pb.js +32 -0
  35. package/dist/gen/src/proto/products/v1/entities/product/shared_pb.d.ts +1042 -0
  36. package/dist/gen/src/proto/products/v1/entities/product/shared_pb.d.ts.map +1 -0
  37. package/dist/gen/src/proto/products/v1/entities/product/shared_pb.js +258 -0
  38. package/dist/gen/src/proto/products/v1/entities/product/update_pb.d.ts +236 -0
  39. package/dist/gen/src/proto/products/v1/entities/product/update_pb.d.ts.map +1 -0
  40. package/dist/gen/src/proto/products/v1/entities/product/update_pb.js +88 -0
  41. package/dist/gen/src/proto/products/v1/entities/tag_pb.d.ts +175 -0
  42. package/dist/gen/src/proto/products/v1/entities/tag_pb.d.ts.map +1 -0
  43. package/dist/gen/src/proto/products/v1/entities/tag_pb.js +84 -0
  44. package/dist/gen/src/proto/products/v1/entities/taxonomy_pb.d.ts +477 -0
  45. package/dist/gen/src/proto/products/v1/entities/taxonomy_pb.d.ts.map +1 -0
  46. package/dist/gen/src/proto/products/v1/entities/taxonomy_pb.js +235 -0
  47. package/dist/gen/src/proto/products/v1/entities/type_pb.d.ts +158 -0
  48. package/dist/gen/src/proto/products/v1/entities/type_pb.d.ts.map +1 -0
  49. package/dist/gen/src/proto/products/v1/entities/type_pb.js +70 -0
  50. package/dist/gen/src/proto/products/v1/entities/variant_pb.d.ts +489 -0
  51. package/dist/gen/src/proto/products/v1/entities/variant_pb.d.ts.map +1 -0
  52. package/dist/gen/src/proto/products/v1/entities/variant_pb.js +147 -0
  53. package/dist/gen/src/proto/products/v1/service_pb.d.ts +316 -0
  54. package/dist/gen/src/proto/products/v1/service_pb.d.ts.map +1 -0
  55. package/dist/gen/src/proto/products/v1/service_pb.js +36 -0
  56. package/dist/src/address.d.ts +53 -0
  57. package/dist/src/address.d.ts.map +1 -0
  58. package/dist/src/address.js +233 -0
  59. package/dist/src/address.spec.d.ts +2 -0
  60. package/dist/src/address.spec.d.ts.map +1 -0
  61. package/dist/src/address.spec.js +488 -0
  62. package/dist/src/auth.d.ts +27 -0
  63. package/dist/src/auth.d.ts.map +1 -0
  64. package/dist/src/auth.js +155 -0
  65. package/dist/src/auth.spec.d.ts +2 -0
  66. package/dist/src/auth.spec.d.ts.map +1 -0
  67. package/dist/src/auth.spec.js +582 -0
  68. package/dist/src/compressor/brotli.d.ts +3 -0
  69. package/dist/src/compressor/brotli.d.ts.map +1 -0
  70. package/dist/src/compressor/brotli.js +30 -0
  71. package/dist/src/index.d.ts +6 -0
  72. package/dist/src/index.d.ts.map +1 -0
  73. package/dist/src/interceptors.d.ts +16 -0
  74. package/dist/src/interceptors.d.ts.map +1 -0
  75. package/dist/src/interceptors.js +156 -0
  76. package/dist/src/interceptors.spec.d.ts +2 -0
  77. package/dist/src/interceptors.spec.d.ts.map +1 -0
  78. package/dist/src/interceptors.spec.js +1063 -0
  79. package/dist/src/policies.d.ts +217 -0
  80. package/dist/src/policies.d.ts.map +1 -0
  81. package/dist/src/policies.js +322 -0
  82. package/dist/src/policies.spec.d.ts +2 -0
  83. package/dist/src/policies.spec.d.ts.map +1 -0
  84. package/dist/src/policies.spec.js +463 -0
  85. package/dist/src/products.d.ts +39 -0
  86. package/dist/src/products.d.ts.map +1 -0
  87. package/dist/src/products.js +95 -0
  88. package/dist/src/products.spec.d.ts +2 -0
  89. package/dist/src/products.spec.d.ts.map +1 -0
  90. package/dist/src/products.spec.js +519 -0
  91. package/dist/src/profile.d.ts +62 -0
  92. package/dist/src/profile.d.ts.map +1 -0
  93. package/dist/src/profile.js +151 -0
  94. package/dist/src/profile.spec.d.ts +2 -0
  95. package/dist/src/profile.spec.d.ts.map +1 -0
  96. package/dist/src/profile.spec.js +475 -0
  97. package/dist/src/registration.d.ts +60 -0
  98. package/dist/src/registration.d.ts.map +1 -0
  99. package/dist/src/registration.js +147 -0
  100. package/dist/src/test-utils.d.ts +87 -0
  101. package/dist/src/test-utils.d.ts.map +1 -0
  102. package/dist/src/test-utils.js +132 -0
  103. package/gen/buf/validate/validate_pb.ts +4799 -0
  104. package/gen/src/proto/auth/v1/authv1connect/service.connect.go +454 -0
  105. package/gen/src/proto/auth/v1/entities/auth.pb.go +818 -0
  106. package/gen/src/proto/auth/v1/entities/auth_pb.ts +348 -0
  107. package/gen/src/proto/auth/v1/entities/policy.pb.go +727 -0
  108. package/gen/src/proto/auth/v1/entities/policy_pb.ts +306 -0
  109. package/gen/src/proto/auth/v1/service-AuthService_connectquery.ts +70 -0
  110. package/gen/src/proto/auth/v1/service.pb.go +119 -0
  111. package/gen/src/proto/auth/v1/service_pb.ts +152 -0
  112. package/gen/src/proto/customers/v1/customersv1connect/service.connect.go +358 -0
  113. package/gen/src/proto/customers/v1/entities/address.pb.go +1073 -0
  114. package/gen/src/proto/customers/v1/entities/address_pb.ts +478 -0
  115. package/gen/src/proto/customers/v1/entities/profile.pb.go +633 -0
  116. package/gen/src/proto/customers/v1/entities/profile_pb.ts +252 -0
  117. package/gen/src/proto/customers/v1/service-CustomerService_connectquery.ts +50 -0
  118. package/gen/src/proto/customers/v1/service.pb.go +110 -0
  119. package/gen/src/proto/customers/v1/service_pb.ts +121 -0
  120. package/gen/src/proto/files/v1/entities/file.pb.go +669 -0
  121. package/gen/src/proto/files/v1/entities/file_pb.ts +265 -0
  122. package/gen/src/proto/files/v1/filesv1connect/service.connect.go +200 -0
  123. package/gen/src/proto/files/v1/service-FileService_connectquery.ts +25 -0
  124. package/gen/src/proto/files/v1/service.pb.go +85 -0
  125. package/gen/src/proto/files/v1/service_pb.ts +65 -0
  126. package/gen/src/proto/products/v1/entities/category.pb.go +744 -0
  127. package/gen/src/proto/products/v1/entities/category_pb.ts +318 -0
  128. package/gen/src/proto/products/v1/entities/collection.pb.go +528 -0
  129. package/gen/src/proto/products/v1/entities/collection_pb.ts +214 -0
  130. package/gen/src/proto/products/v1/entities/product/create.pb.go +453 -0
  131. package/gen/src/proto/products/v1/entities/product/create_pb.ts +199 -0
  132. package/gen/src/proto/products/v1/entities/product/option.pb.go +206 -0
  133. package/gen/src/proto/products/v1/entities/product/option_pb.ts +74 -0
  134. package/gen/src/proto/products/v1/entities/product/shared.pb.go +2890 -0
  135. package/gen/src/proto/products/v1/entities/product/shared_pb.ts +1317 -0
  136. package/gen/src/proto/products/v1/entities/product/update.pb.go +794 -0
  137. package/gen/src/proto/products/v1/entities/product/update_pb.ts +325 -0
  138. package/gen/src/proto/products/v1/entities/tag.pb.go +610 -0
  139. package/gen/src/proto/products/v1/entities/tag_pb.ts +233 -0
  140. package/gen/src/proto/products/v1/entities/taxonomy.pb.go +1352 -0
  141. package/gen/src/proto/products/v1/entities/taxonomy_pb.ts +606 -0
  142. package/gen/src/proto/products/v1/entities/type.pb.go +553 -0
  143. package/gen/src/proto/products/v1/entities/type_pb.ts +215 -0
  144. package/gen/src/proto/products/v1/entities/variant.pb.go +1474 -0
  145. package/gen/src/proto/products/v1/entities/variant_pb.ts +629 -0
  146. package/gen/src/proto/products/v1/productsv1connect/service.connect.go +1195 -0
  147. package/gen/src/proto/products/v1/service-ProductsService_connectquery.ts +199 -0
  148. package/gen/src/proto/products/v1/service.pb.go +250 -0
  149. package/gen/src/proto/products/v1/service_pb.ts +429 -0
  150. package/gen/src/proto/promotions/v1/entities/campaign.pb.go +1229 -0
  151. package/gen/src/proto/promotions/v1/entities/campaign_pb.ts +511 -0
  152. package/gen/src/proto/promotions/v1/promotionsv1connect/service.connect.go +289 -0
  153. package/gen/src/proto/promotions/v1/service-CampaignService_connectquery.ts +42 -0
  154. package/gen/src/proto/promotions/v1/service.pb.go +98 -0
  155. package/gen/src/proto/promotions/v1/service_pb.ts +95 -0
  156. package/gen/src/proto/stocknodes/v1/entities/stocknode.pb.go +1499 -0
  157. package/gen/src/proto/stocknodes/v1/entities/stocknode_pb.ts +655 -0
  158. package/gen/src/proto/stocknodes/v1/service-StockNodesService_connectquery.ts +41 -0
  159. package/gen/src/proto/stocknodes/v1/service.pb.go +98 -0
  160. package/gen/src/proto/stocknodes/v1/service_pb.ts +95 -0
  161. package/gen/src/proto/stocknodes/v1/stocknodesv1connect/service.connect.go +297 -0
  162. package/package.json +5 -2
  163. package/src/address.spec.ts +0 -662
  164. package/src/address.ts +0 -300
  165. package/src/auth.spec.ts +0 -771
  166. package/src/auth.ts +0 -168
  167. package/src/compressor/brotli.ts +0 -26
  168. package/src/index.ts +0 -5
  169. package/src/interceptors.spec.ts +0 -1343
  170. package/src/interceptors.ts +0 -224
  171. package/src/policies.spec.ts +0 -595
  172. package/src/policies.ts +0 -431
  173. package/src/products.spec.ts +0 -710
  174. package/src/products.ts +0 -112
  175. package/src/profile.spec.ts +0 -626
  176. package/src/profile.ts +0 -169
  177. package/src/proto/auth/v1/entities/auth.proto +0 -140
  178. package/src/proto/auth/v1/entities/policy.proto +0 -57
  179. package/src/proto/auth/v1/service.proto +0 -26
  180. package/src/proto/customers/v1/entities/address.proto +0 -101
  181. package/src/proto/customers/v1/entities/profile.proto +0 -118
  182. package/src/proto/customers/v1/service.proto +0 -36
  183. package/src/proto/files/v1/entities/file.proto +0 -62
  184. package/src/proto/files/v1/service.proto +0 -19
  185. package/src/proto/products/v1/entities/category.proto +0 -98
  186. package/src/proto/products/v1/entities/collection.proto +0 -72
  187. package/src/proto/products/v1/entities/product/create.proto +0 -41
  188. package/src/proto/products/v1/entities/product/option.proto +0 -17
  189. package/src/proto/products/v1/entities/product/shared.proto +0 -255
  190. package/src/proto/products/v1/entities/product/update.proto +0 -66
  191. package/src/proto/products/v1/entities/tag.proto +0 -73
  192. package/src/proto/products/v1/entities/taxonomy.proto +0 -146
  193. package/src/proto/products/v1/entities/type.proto +0 -98
  194. package/src/proto/products/v1/entities/variant.proto +0 -127
  195. package/src/proto/products/v1/service.proto +0 -78
  196. package/src/proto/promotions/v1/entities/campaign.proto +0 -145
  197. package/src/proto/promotions/v1/service.proto +0 -17
  198. package/src/proto/stocknodes/v1/entities/stocknode.proto +0 -167
  199. package/src/proto/stocknodes/v1/service.proto +0 -21
  200. package/src/registration.ts +0 -170
  201. package/src/test-utils.ts +0 -176
@@ -1,1343 +0,0 @@
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
- })