@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
package/src/auth.spec.ts
ADDED
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { createClient } from '@connectrpc/connect'
|
|
2
|
+
import { createGrpcTransport } from '@connectrpc/connect-node'
|
|
3
|
+
import { getCheckoutConfig } from '@paakd/config'
|
|
4
|
+
import {
|
|
5
|
+
changePassword,
|
|
6
|
+
login,
|
|
7
|
+
refreshToken,
|
|
8
|
+
type ChangePasswordRequestProps,
|
|
9
|
+
type LoginRequestProps,
|
|
10
|
+
} from './auth'
|
|
11
|
+
import {
|
|
12
|
+
createAuthenticationInterceptor,
|
|
13
|
+
createCustomerAuthenticationInterceptor,
|
|
14
|
+
createHeadersInterceptor,
|
|
15
|
+
} from './interceptors'
|
|
16
|
+
import {
|
|
17
|
+
type BaseTestContext,
|
|
18
|
+
type Checkout,
|
|
19
|
+
type MockedFunction,
|
|
20
|
+
type MockServiceClient,
|
|
21
|
+
type Transport,
|
|
22
|
+
clearAllMocks,
|
|
23
|
+
createMockConnectError,
|
|
24
|
+
setupCommonMocks,
|
|
25
|
+
} from './test-utils'
|
|
26
|
+
|
|
27
|
+
// Mock dependencies
|
|
28
|
+
vi.mock('@connectrpc/connect', async () => {
|
|
29
|
+
const actual = await vi.importActual('@connectrpc/connect')
|
|
30
|
+
return {
|
|
31
|
+
...actual,
|
|
32
|
+
createClient: vi.fn(),
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
vi.mock('./compressor/brotli', () => ({
|
|
37
|
+
brotliCompression: {
|
|
38
|
+
name: 'brotli',
|
|
39
|
+
compress: vi.fn(),
|
|
40
|
+
decompress: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
vi.mock('@connectrpc/connect-node', () => ({
|
|
45
|
+
createGrpcTransport: vi.fn(),
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
vi.mock('@paakd/config', () => ({
|
|
49
|
+
getCheckoutConfig: vi.fn(),
|
|
50
|
+
}))
|
|
51
|
+
|
|
52
|
+
vi.mock('./interceptors', () => ({
|
|
53
|
+
createAuthenticationInterceptor: vi.fn(),
|
|
54
|
+
createCustomerAuthenticationInterceptor: vi.fn(),
|
|
55
|
+
createHeadersInterceptor: vi.fn(),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
vi.mock('../gen/src/proto/auth/v1/service_pb', () => ({
|
|
59
|
+
AuthService: {},
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
vi.mock('../gen/src/proto/customers/v1/service_pb', () => ({
|
|
63
|
+
CustomerService: {},
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
const mockGetCheckoutConfig = vi.mocked(getCheckoutConfig)
|
|
67
|
+
const mockCreateGrpcTransport = vi.mocked(createGrpcTransport)
|
|
68
|
+
const mockCreateClient = vi.mocked(createClient)
|
|
69
|
+
const mockCreateAuthenticationInterceptor = vi.mocked(
|
|
70
|
+
createAuthenticationInterceptor
|
|
71
|
+
)
|
|
72
|
+
const mockCreateCustomerAuthenticationInterceptor = vi.mocked(
|
|
73
|
+
createCustomerAuthenticationInterceptor
|
|
74
|
+
)
|
|
75
|
+
const mockCreateHeadersInterceptor = vi.mocked(createHeadersInterceptor)
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Extended test context for auth service
|
|
79
|
+
*/
|
|
80
|
+
interface AuthTestContext extends BaseTestContext {
|
|
81
|
+
clients: {
|
|
82
|
+
auth: MockServiceClient
|
|
83
|
+
customer: MockServiceClient
|
|
84
|
+
}
|
|
85
|
+
config: Checkout
|
|
86
|
+
interceptors: Record<string, MockedFunction<(...args: unknown[]) => unknown>>
|
|
87
|
+
transport: Transport
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Setup function similar to testing-library's render.
|
|
92
|
+
* Initializes the testing environment with proper mocks
|
|
93
|
+
*/
|
|
94
|
+
function setupAuthService(): AuthTestContext {
|
|
95
|
+
clearAllMocks()
|
|
96
|
+
const { config, interceptors, transport } = setupCommonMocks()
|
|
97
|
+
|
|
98
|
+
const clients = {
|
|
99
|
+
auth: {
|
|
100
|
+
login: vi.fn(),
|
|
101
|
+
refreshToken: vi.fn(),
|
|
102
|
+
},
|
|
103
|
+
customer: {
|
|
104
|
+
changePassword: vi.fn(),
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Setup mock implementations
|
|
109
|
+
mockGetCheckoutConfig.mockResolvedValue(config)
|
|
110
|
+
mockCreateGrpcTransport.mockReturnValue(transport)
|
|
111
|
+
mockCreateHeadersInterceptor.mockImplementation(() => next => async req => {
|
|
112
|
+
return await next(req)
|
|
113
|
+
})
|
|
114
|
+
mockCreateAuthenticationInterceptor.mockImplementation(
|
|
115
|
+
() => next => async req => {
|
|
116
|
+
return await next(req)
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
mockCreateCustomerAuthenticationInterceptor.mockImplementation(
|
|
120
|
+
() => next => async req => {
|
|
121
|
+
return await next(req)
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// mockCreateClient should intelligently return the right client
|
|
126
|
+
// The tests will set up their own expectations, but provide a smart default
|
|
127
|
+
mockCreateClient.mockImplementation(() => {
|
|
128
|
+
// Default to returning auth client, but tests can override as needed
|
|
129
|
+
return clients.auth as any
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
client: clients.auth,
|
|
134
|
+
clients,
|
|
135
|
+
config,
|
|
136
|
+
interceptors,
|
|
137
|
+
transport,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
describe('Authentication Service', () => {
|
|
142
|
+
let consoleSpySpy: any
|
|
143
|
+
|
|
144
|
+
beforeEach(() => {
|
|
145
|
+
consoleSpySpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
consoleSpySpy.mockRestore()
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe('login', () => {
|
|
153
|
+
it('should successfully authenticate user with valid credentials', async () => {
|
|
154
|
+
const { clients } = setupAuthService()
|
|
155
|
+
const credentials: LoginRequestProps = {
|
|
156
|
+
body: {
|
|
157
|
+
email: 'user@example.com',
|
|
158
|
+
password: 'password123',
|
|
159
|
+
},
|
|
160
|
+
headers: {
|
|
161
|
+
'x-shop-id': '123',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const expectedResponse = {
|
|
166
|
+
jwt: 'token123',
|
|
167
|
+
refreshToken: 'refresh123',
|
|
168
|
+
expiresIn: 3600,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clients.auth.login.mockResolvedValue(expectedResponse)
|
|
172
|
+
|
|
173
|
+
const result = await login(credentials)
|
|
174
|
+
|
|
175
|
+
expect((result as any).status).toBe('success')
|
|
176
|
+
expect(result).toHaveProperty('value')
|
|
177
|
+
expect(clients.auth.login).toHaveBeenCalledWith({
|
|
178
|
+
email: credentials.body.email,
|
|
179
|
+
password: credentials.body.password,
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should reject user when credentials are invalid', async () => {
|
|
184
|
+
const { clients } = setupAuthService()
|
|
185
|
+
const error = createMockConnectError(
|
|
186
|
+
16,
|
|
187
|
+
'UNAUTHENTICATED',
|
|
188
|
+
'Invalid credentials'
|
|
189
|
+
)
|
|
190
|
+
clients.auth.login.mockRejectedValue(error)
|
|
191
|
+
|
|
192
|
+
const result = await login({
|
|
193
|
+
body: {
|
|
194
|
+
email: 'user@example.com',
|
|
195
|
+
password: 'wrongpassword',
|
|
196
|
+
},
|
|
197
|
+
headers: {},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
expect((result as any).status).toBe('failed')
|
|
201
|
+
expect(result).toHaveProperty('code')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should handle network failures gracefully', async () => {
|
|
205
|
+
const { clients } = setupAuthService()
|
|
206
|
+
const networkError = createMockConnectError(
|
|
207
|
+
14,
|
|
208
|
+
'UNAVAILABLE',
|
|
209
|
+
'Service unavailable'
|
|
210
|
+
)
|
|
211
|
+
clients.auth.login.mockRejectedValue(networkError)
|
|
212
|
+
|
|
213
|
+
const result = await login({
|
|
214
|
+
body: {
|
|
215
|
+
email: 'user@example.com',
|
|
216
|
+
password: 'password123',
|
|
217
|
+
},
|
|
218
|
+
headers: {},
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
expect(result.status).toBe('failed')
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should include custom headers in request context', async () => {
|
|
225
|
+
const { clients } = setupAuthService()
|
|
226
|
+
const customHeaders = {
|
|
227
|
+
'x-original-host': 'example.com',
|
|
228
|
+
'x-shop-id': '456',
|
|
229
|
+
'x-locale': 'en-US',
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
clients.auth.login.mockResolvedValue({
|
|
233
|
+
jwt: 'new-token',
|
|
234
|
+
refreshToken: 'refresh-token',
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
await login({
|
|
238
|
+
body: {
|
|
239
|
+
email: 'user@example.com',
|
|
240
|
+
password: 'password123',
|
|
241
|
+
},
|
|
242
|
+
headers: customHeaders,
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expect(mockCreateHeadersInterceptor).toHaveBeenCalledWith(customHeaders)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('should use checkout configuration for enterprise URL', async () => {
|
|
249
|
+
const { clients, config } = setupAuthService()
|
|
250
|
+
|
|
251
|
+
const jwt = 'token-xyz'
|
|
252
|
+
clients.auth.login.mockResolvedValue({ jwt })
|
|
253
|
+
|
|
254
|
+
await login({
|
|
255
|
+
body: {
|
|
256
|
+
email: 'user@example.com',
|
|
257
|
+
password: 'password123',
|
|
258
|
+
},
|
|
259
|
+
headers: {},
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
expect(mockCreateGrpcTransport).toHaveBeenCalledWith(
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
baseUrl: config.enterpriseURL,
|
|
265
|
+
})
|
|
266
|
+
)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should enable compression for optimized data transfer', async () => {
|
|
270
|
+
const { clients } = setupAuthService()
|
|
271
|
+
|
|
272
|
+
const jwt = 'token-for-compression-test'
|
|
273
|
+
clients.auth.login.mockResolvedValue({ jwt })
|
|
274
|
+
|
|
275
|
+
await login({
|
|
276
|
+
body: {
|
|
277
|
+
email: 'user@example.com',
|
|
278
|
+
password: 'password123',
|
|
279
|
+
},
|
|
280
|
+
headers: {},
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const transportConfig = (mockCreateGrpcTransport as any).mock.calls[0][0]
|
|
284
|
+
expect(transportConfig).toHaveProperty('acceptCompression')
|
|
285
|
+
expect(transportConfig).toHaveProperty('sendCompression')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('should handle service unavailability', async () => {
|
|
289
|
+
const { clients } = setupAuthService()
|
|
290
|
+
const error = createMockConnectError(
|
|
291
|
+
14,
|
|
292
|
+
'UNAVAILABLE',
|
|
293
|
+
'Service temporarily unavailable'
|
|
294
|
+
)
|
|
295
|
+
clients.auth.login.mockRejectedValue(error)
|
|
296
|
+
|
|
297
|
+
const result = await login({
|
|
298
|
+
body: {
|
|
299
|
+
email: 'user@example.com',
|
|
300
|
+
password: 'password123',
|
|
301
|
+
},
|
|
302
|
+
headers: {},
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
expect(result.status).toBe('failed')
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('should support multiple concurrent login attempts', async () => {
|
|
309
|
+
const { clients } = setupAuthService()
|
|
310
|
+
const mockResponse = {
|
|
311
|
+
jwt: 'concurrent-token',
|
|
312
|
+
refreshToken: 'concurrent-refresh',
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
clients.auth.login.mockResolvedValue(mockResponse)
|
|
316
|
+
|
|
317
|
+
const results = await Promise.all([
|
|
318
|
+
login({
|
|
319
|
+
body: {
|
|
320
|
+
email: 'user1@example.com',
|
|
321
|
+
password: 'password123',
|
|
322
|
+
},
|
|
323
|
+
headers: {},
|
|
324
|
+
}),
|
|
325
|
+
login({
|
|
326
|
+
body: {
|
|
327
|
+
email: 'user2@example.com',
|
|
328
|
+
password: 'password456',
|
|
329
|
+
},
|
|
330
|
+
headers: {},
|
|
331
|
+
}),
|
|
332
|
+
login({
|
|
333
|
+
body: {
|
|
334
|
+
email: 'user3@example.com',
|
|
335
|
+
password: 'password789',
|
|
336
|
+
},
|
|
337
|
+
headers: {},
|
|
338
|
+
}),
|
|
339
|
+
])
|
|
340
|
+
|
|
341
|
+
expect(results).toHaveLength(3)
|
|
342
|
+
expect(results.every(r => (r as any).status === 'success')).toBe(true)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
describe('refreshToken', () => {
|
|
347
|
+
it('should refresh authentication token with valid refresh token', async () => {
|
|
348
|
+
const { clients } = setupAuthService()
|
|
349
|
+
const refreshTokenValue = 'refresh-token-123'
|
|
350
|
+
|
|
351
|
+
const expectedResponse = {
|
|
352
|
+
jwt: 'new-jwt-token',
|
|
353
|
+
refreshToken: 'new-refresh-token',
|
|
354
|
+
expiresIn: 3600,
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
clients.auth.refreshToken.mockResolvedValue(expectedResponse)
|
|
358
|
+
|
|
359
|
+
const result = await refreshToken({
|
|
360
|
+
body: {
|
|
361
|
+
refreshToken: refreshTokenValue,
|
|
362
|
+
},
|
|
363
|
+
headers: {},
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
expect((result as any).status).toBe('success')
|
|
367
|
+
expect(clients.auth.refreshToken).toHaveBeenCalledWith({
|
|
368
|
+
refreshToken: refreshTokenValue,
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('should reject expired refresh tokens', async () => {
|
|
373
|
+
const { clients } = setupAuthService()
|
|
374
|
+
const error = createMockConnectError(
|
|
375
|
+
16,
|
|
376
|
+
'UNAUTHENTICATED',
|
|
377
|
+
'Refresh token expired'
|
|
378
|
+
)
|
|
379
|
+
clients.auth.refreshToken.mockRejectedValue(error)
|
|
380
|
+
|
|
381
|
+
const result = await refreshToken({
|
|
382
|
+
body: {
|
|
383
|
+
refreshToken: 'expired-refresh-token',
|
|
384
|
+
},
|
|
385
|
+
headers: {},
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
expect((result as any).status).toBe('failed')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('should handle missing or malformed refresh tokens', async () => {
|
|
392
|
+
const { clients } = setupAuthService()
|
|
393
|
+
const error = createMockConnectError(
|
|
394
|
+
3,
|
|
395
|
+
'INVALID_ARGUMENT',
|
|
396
|
+
'Invalid refresh token format'
|
|
397
|
+
)
|
|
398
|
+
clients.auth.refreshToken.mockRejectedValue(error)
|
|
399
|
+
|
|
400
|
+
const result = await refreshToken({
|
|
401
|
+
body: {
|
|
402
|
+
refreshToken: '',
|
|
403
|
+
},
|
|
404
|
+
headers: {},
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
expect((result as any).status).toBe('failed')
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should recover gracefully from service errors', async () => {
|
|
411
|
+
const { clients } = setupAuthService()
|
|
412
|
+
const serviceError = createMockConnectError(
|
|
413
|
+
13,
|
|
414
|
+
'INTERNAL',
|
|
415
|
+
'Internal server error'
|
|
416
|
+
)
|
|
417
|
+
clients.auth.refreshToken.mockRejectedValue(serviceError)
|
|
418
|
+
|
|
419
|
+
const result = await refreshToken({
|
|
420
|
+
body: {
|
|
421
|
+
refreshToken: 'valid-refresh-token',
|
|
422
|
+
},
|
|
423
|
+
headers: {},
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
expect((result as any).status).toBe('failed')
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('should use authentication interceptor for token refresh', async () => {
|
|
430
|
+
const { clients } = setupAuthService()
|
|
431
|
+
const refreshTokenValue = 'refresh-token-abc'
|
|
432
|
+
|
|
433
|
+
clients.auth.refreshToken.mockResolvedValue({
|
|
434
|
+
jwt: 'new-jwt-token',
|
|
435
|
+
refreshToken: 'new-refresh-token',
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
await refreshToken({
|
|
439
|
+
body: {
|
|
440
|
+
refreshToken: refreshTokenValue,
|
|
441
|
+
},
|
|
442
|
+
headers: {},
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
expect(mockCreateAuthenticationInterceptor).toHaveBeenCalled()
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
describe('changePassword', () => {
|
|
450
|
+
it('should successfully change user password with correct current password', async () => {
|
|
451
|
+
const { clients } = setupAuthService()
|
|
452
|
+
const passwordChange: ChangePasswordRequestProps = {
|
|
453
|
+
body: {
|
|
454
|
+
jwt: 'user-jwt-token',
|
|
455
|
+
customerId: 'cust-1',
|
|
456
|
+
oldPassword: 'oldpassword123',
|
|
457
|
+
newPassword: 'newpassword456',
|
|
458
|
+
} as any,
|
|
459
|
+
headers: {},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const success = { jwt: 'updated-jwt-token' }
|
|
463
|
+
clients.customer.changePassword.mockResolvedValue(success)
|
|
464
|
+
|
|
465
|
+
// For changePassword test, we need mockCreateClient to return customer client
|
|
466
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
467
|
+
|
|
468
|
+
const result = await changePassword(passwordChange)
|
|
469
|
+
|
|
470
|
+
expect((result as any).status).toBe('success')
|
|
471
|
+
expect(result).toHaveProperty('value')
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('should reject when current password is incorrect', async () => {
|
|
475
|
+
const { clients } = setupAuthService()
|
|
476
|
+
const error = createMockConnectError(
|
|
477
|
+
16,
|
|
478
|
+
'UNAUTHENTICATED',
|
|
479
|
+
'Current password is incorrect'
|
|
480
|
+
)
|
|
481
|
+
clients.customer.changePassword.mockRejectedValue(error)
|
|
482
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
483
|
+
|
|
484
|
+
const result = await changePassword({
|
|
485
|
+
body: {
|
|
486
|
+
jwt: 'user-jwt-token',
|
|
487
|
+
customerId: 'cust-1',
|
|
488
|
+
oldPassword: 'wrongpassword',
|
|
489
|
+
newPassword: 'newpassword456',
|
|
490
|
+
} as any,
|
|
491
|
+
headers: {},
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
expect((result as any).status).toBe('failed')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('should enforce password complexity requirements', async () => {
|
|
498
|
+
const { clients } = setupAuthService()
|
|
499
|
+
const error = createMockConnectError(
|
|
500
|
+
3,
|
|
501
|
+
'INVALID_ARGUMENT',
|
|
502
|
+
'Password does not meet complexity requirements'
|
|
503
|
+
)
|
|
504
|
+
clients.customer.changePassword.mockRejectedValue(error)
|
|
505
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
506
|
+
|
|
507
|
+
const result = await changePassword({
|
|
508
|
+
body: {
|
|
509
|
+
jwt: 'user-jwt-token',
|
|
510
|
+
customerId: 'cust-1',
|
|
511
|
+
oldPassword: 'oldpassword123',
|
|
512
|
+
newPassword: 'weak',
|
|
513
|
+
} as any,
|
|
514
|
+
headers: {},
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
expect((result as any).status).toBe('failed')
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('should reject requests with invalid or expired JWT', async () => {
|
|
521
|
+
const { clients } = setupAuthService()
|
|
522
|
+
const error = createMockConnectError(
|
|
523
|
+
16,
|
|
524
|
+
'UNAUTHENTICATED',
|
|
525
|
+
'Invalid or expired JWT'
|
|
526
|
+
)
|
|
527
|
+
clients.customer.changePassword.mockRejectedValue(error)
|
|
528
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
529
|
+
|
|
530
|
+
const result = await changePassword({
|
|
531
|
+
body: {
|
|
532
|
+
jwt: 'expired-jwt-token',
|
|
533
|
+
customerId: 'cust-1',
|
|
534
|
+
oldPassword: 'oldpassword123',
|
|
535
|
+
newPassword: 'newpassword456',
|
|
536
|
+
} as any,
|
|
537
|
+
headers: {},
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
expect((result as any).status).toBe('failed')
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
it('should not include JWT in service request', async () => {
|
|
544
|
+
const { clients } = setupAuthService()
|
|
545
|
+
const success = { jwt: 'updated-jwt-token' }
|
|
546
|
+
clients.customer.changePassword.mockResolvedValue(success)
|
|
547
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
548
|
+
|
|
549
|
+
const jwtToken = 'user-jwt-token'
|
|
550
|
+
const passwordData: ChangePasswordRequestProps = {
|
|
551
|
+
body: {
|
|
552
|
+
jwt: jwtToken,
|
|
553
|
+
customerId: 'cust-1',
|
|
554
|
+
oldPassword: 'oldpassword123',
|
|
555
|
+
newPassword: 'newpassword456',
|
|
556
|
+
} as any,
|
|
557
|
+
headers: {},
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
await changePassword(passwordData)
|
|
561
|
+
|
|
562
|
+
// Verify that JWT is not passed directly to the service
|
|
563
|
+
const callArgs = clients.customer.changePassword.mock.calls[0][0]
|
|
564
|
+
expect(callArgs).not.toHaveProperty('jwt')
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('should apply three interceptors in correct order', async () => {
|
|
568
|
+
const { clients, interceptors } = setupAuthService()
|
|
569
|
+
const success = { jwt: 'updated-jwt-token' }
|
|
570
|
+
clients.customer.changePassword.mockResolvedValue(success)
|
|
571
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
572
|
+
|
|
573
|
+
const headers = {
|
|
574
|
+
'x-shop-id': '123',
|
|
575
|
+
}
|
|
576
|
+
const jwt = 'user-jwt-token'
|
|
577
|
+
|
|
578
|
+
await changePassword({
|
|
579
|
+
body: {
|
|
580
|
+
customerId: 'cust-1',
|
|
581
|
+
oldPassword: 'oldpassword123',
|
|
582
|
+
newPassword: 'newpassword456',
|
|
583
|
+
jwt,
|
|
584
|
+
} as any,
|
|
585
|
+
headers,
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
expect(mockCreateHeadersInterceptor).toHaveBeenCalledWith(headers)
|
|
589
|
+
expect(mockCreateAuthenticationInterceptor).toHaveBeenCalled()
|
|
590
|
+
expect(mockCreateCustomerAuthenticationInterceptor).toHaveBeenCalledWith(
|
|
591
|
+
jwt
|
|
592
|
+
)
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
it('should handle database errors gracefully', async () => {
|
|
596
|
+
const { clients } = setupAuthService()
|
|
597
|
+
const dbError = createMockConnectError(
|
|
598
|
+
13,
|
|
599
|
+
'INTERNAL',
|
|
600
|
+
'Database error occurred'
|
|
601
|
+
)
|
|
602
|
+
clients.customer.changePassword.mockRejectedValue(dbError)
|
|
603
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
604
|
+
|
|
605
|
+
const result = await changePassword({
|
|
606
|
+
body: {
|
|
607
|
+
jwt: 'user-jwt-token',
|
|
608
|
+
customerId: 'cust-1',
|
|
609
|
+
oldPassword: 'oldpassword123',
|
|
610
|
+
newPassword: 'newpassword456',
|
|
611
|
+
} as any,
|
|
612
|
+
headers: {},
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
expect((result as any).status).toBe('failed')
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
it('should handle permission errors when user cannot change password', async () => {
|
|
619
|
+
const { clients } = setupAuthService()
|
|
620
|
+
const error = createMockConnectError(
|
|
621
|
+
7,
|
|
622
|
+
'PERMISSION_DENIED',
|
|
623
|
+
'User does not have permission to change password'
|
|
624
|
+
)
|
|
625
|
+
clients.customer.changePassword.mockRejectedValue(error)
|
|
626
|
+
mockCreateClient.mockReturnValue(clients.customer as any)
|
|
627
|
+
|
|
628
|
+
const result = await changePassword({
|
|
629
|
+
body: {
|
|
630
|
+
jwt: 'restricted-jwt-token',
|
|
631
|
+
customerId: 'cust-1',
|
|
632
|
+
oldPassword: 'oldpassword123',
|
|
633
|
+
newPassword: 'newpassword456',
|
|
634
|
+
} as any,
|
|
635
|
+
headers: {},
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
expect(result.status).toBe('failed')
|
|
639
|
+
})
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
describe('Common Authentication Behavior', () => {
|
|
643
|
+
it('should load configuration once per request', async () => {
|
|
644
|
+
const { clients } = setupAuthService()
|
|
645
|
+
clients.auth.login.mockResolvedValue({
|
|
646
|
+
jwt: 'token123',
|
|
647
|
+
refreshToken: 'refresh123',
|
|
648
|
+
})
|
|
649
|
+
|
|
650
|
+
await login({
|
|
651
|
+
body: {
|
|
652
|
+
email: 'user@example.com',
|
|
653
|
+
password: 'password123',
|
|
654
|
+
},
|
|
655
|
+
headers: {},
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
await login({
|
|
659
|
+
body: {
|
|
660
|
+
email: 'user2@example.com',
|
|
661
|
+
password: 'password456',
|
|
662
|
+
},
|
|
663
|
+
headers: {},
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
expect(mockGetCheckoutConfig).toHaveBeenCalledTimes(2)
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
it('should support different error scenarios across functions', async () => {
|
|
670
|
+
const { clients } = setupAuthService()
|
|
671
|
+
|
|
672
|
+
const errorScenarios = [
|
|
673
|
+
{
|
|
674
|
+
code: 16,
|
|
675
|
+
message: 'UNAUTHENTICATED',
|
|
676
|
+
fn: login,
|
|
677
|
+
args: {
|
|
678
|
+
body: {
|
|
679
|
+
email: 'user@example.com',
|
|
680
|
+
password: 'invalid',
|
|
681
|
+
},
|
|
682
|
+
headers: {},
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
code: 16,
|
|
687
|
+
message: 'UNAUTHENTICATED',
|
|
688
|
+
fn: refreshToken,
|
|
689
|
+
args: {
|
|
690
|
+
body: {
|
|
691
|
+
refreshToken: 'expired-token',
|
|
692
|
+
},
|
|
693
|
+
headers: {},
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
code: 16,
|
|
698
|
+
message: 'UNAUTHENTICATED',
|
|
699
|
+
fn: changePassword,
|
|
700
|
+
args: {
|
|
701
|
+
body: {
|
|
702
|
+
jwt: 'invalid-jwt',
|
|
703
|
+
customerId: 'cust-1',
|
|
704
|
+
oldPassword: 'old',
|
|
705
|
+
newPassword: 'new',
|
|
706
|
+
} as any,
|
|
707
|
+
headers: {},
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
]
|
|
711
|
+
|
|
712
|
+
for (const scenario of errorScenarios) {
|
|
713
|
+
const error = createMockConnectError(
|
|
714
|
+
scenario.code,
|
|
715
|
+
scenario.message,
|
|
716
|
+
scenario.message
|
|
717
|
+
)
|
|
718
|
+
|
|
719
|
+
if (scenario.fn === login) {
|
|
720
|
+
clients.auth.login.mockRejectedValueOnce(error)
|
|
721
|
+
} else if (scenario.fn === refreshToken) {
|
|
722
|
+
clients.auth.refreshToken.mockRejectedValueOnce(error)
|
|
723
|
+
} else {
|
|
724
|
+
clients.customer.changePassword.mockRejectedValueOnce(error)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const result = await scenario.fn(scenario.args as any)
|
|
728
|
+
expect((result as any).status).toBe('failed')
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
it('should handle edge cases with special characters and long inputs', async () => {
|
|
733
|
+
const { clients } = setupAuthService()
|
|
734
|
+
const jwt = 'special-chars-token-!@#$%^&*()'
|
|
735
|
+
clients.auth.login.mockResolvedValue({
|
|
736
|
+
jwt: 'new-token',
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
const result = await login({
|
|
740
|
+
body: {
|
|
741
|
+
email: 'user+tag@example.com',
|
|
742
|
+
password: 'P@ssw0rd!@#$%^&*()',
|
|
743
|
+
},
|
|
744
|
+
headers: {},
|
|
745
|
+
})
|
|
746
|
+
|
|
747
|
+
expect(result.status).toBe('success')
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('should handle null or undefined headers gracefully', async () => {
|
|
751
|
+
const { clients } = setupAuthService()
|
|
752
|
+
const jwt = 'test-jwt-token'
|
|
753
|
+
clients.auth.login.mockResolvedValue({
|
|
754
|
+
jwt: 'token123',
|
|
755
|
+
refreshToken: 'refresh123',
|
|
756
|
+
})
|
|
757
|
+
|
|
758
|
+
const result = await login({
|
|
759
|
+
body: {
|
|
760
|
+
email: 'user@example.com',
|
|
761
|
+
password: 'password123',
|
|
762
|
+
},
|
|
763
|
+
headers: {
|
|
764
|
+
'x-header': null,
|
|
765
|
+
},
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
expect((result as any).status).toBe('success')
|
|
769
|
+
})
|
|
770
|
+
})
|
|
771
|
+
})
|