@api-client/core 0.18.34 → 0.18.36

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.
@@ -0,0 +1,955 @@
1
+ import { nanoid } from '../nanoid.js'
2
+ import type { IOrganization } from '../models/store/Organization.js'
3
+ import type { GroupSchema } from '../models/store/Group.js'
4
+ import type { IUser } from '../models/store/User.js'
5
+ import type { InvitationSchema } from '../models/store/Invitation.js'
6
+ import { OrganizationKind, GroupKind, InvitationKind } from '../models/kinds.js'
7
+ import { Kind as UserKind } from '../models/store/User.js'
8
+ import { File, type IFile, type FileBreadcrumb } from '../models/store/File.js'
9
+ import { CertificateFileKind, DomainFileKind, FolderKind, ProjectKind } from '../models/kinds.js'
10
+ import type { ContextListResult, IBulkOperationResult } from '../events/BaseEvents.js'
11
+ import type { MediaPatchRevision } from '../patch/types.js'
12
+ import type { IFolder } from '../models/Folder.js'
13
+ import type { TrashEntry } from '../models/TrashEntry.js'
14
+ import type { StoreSdk } from './StoreSdkWeb.js'
15
+ import * as sinon from 'sinon'
16
+ import { Exception } from '../exceptions/exception.js'
17
+
18
+ /**
19
+ * Options for customizing mock responses.
20
+ */
21
+ export interface MockResponseOptions {
22
+ /**
23
+ * Custom response data to return instead of generated random data.
24
+ */
25
+ data?: unknown
26
+ /**
27
+ * HTTP status code to return. Defaults to 200.
28
+ */
29
+ status?: number
30
+ /**
31
+ * Custom headers to include in the response.
32
+ */
33
+ headers?: Record<string, string>
34
+ }
35
+
36
+ /**
37
+ * A stub reference that can be used to restore the original behavior.
38
+ */
39
+ export interface StubReference {
40
+ /**
41
+ * Restores the original behavior.
42
+ */
43
+ restore: () => void
44
+ }
45
+
46
+ /**
47
+ * SDK mocking utility for testing. Provides simple API to mock SDK calls
48
+ * with random or custom responses.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { SdkMock } from '@api-client/core/sdk/SdkMock.js';
53
+ * import { StoreSdk } from '@api-client/core/sdk/StoreSdkWeb.js';
54
+ *
55
+ * const sdk = new StoreSdk('http://localhost:8080');
56
+ * const mocker = new SdkMock(sdk);
57
+ *
58
+ * // Simple usage - returns random valid organization
59
+ * const stub1 = mocker.organizations.list();
60
+ *
61
+ * // Custom response
62
+ * const stub2 = mocker.organizations.create({
63
+ * data: { key: 'org-1', name: 'Test Org', ... }
64
+ * });
65
+ *
66
+ * // Custom status and headers
67
+ * const stub3 = mocker.users.me({
68
+ * status: 404,
69
+ * headers: { 'X-Custom': 'value' }
70
+ * });
71
+ *
72
+ * // Restore all stubs
73
+ * mocker.restore();
74
+ * ```
75
+ */
76
+ export class SdkMock {
77
+ private stubs: StubReference[] = []
78
+
79
+ constructor(private sdk: StoreSdk) {}
80
+
81
+ /**
82
+ * Organization API mocks.
83
+ */
84
+ organizations = {
85
+ /**
86
+ * Mocks the `organizations.list()` method.
87
+ * @param options Optional response customization.
88
+ * @returns A stub reference that can be used to restore the original behavior.
89
+ */
90
+ list: (options?: MockResponseOptions): StubReference => {
91
+ return this.createStub('organizations', 'list', () => {
92
+ const defaultData = {
93
+ items: [this.generateOrganization(), this.generateOrganization()],
94
+ nextPageToken: undefined,
95
+ }
96
+ if (options?.status !== undefined && options.status !== 200) {
97
+ throw new Exception('Mocked error', { status: options.status })
98
+ }
99
+ return (options?.data ?? defaultData) as unknown
100
+ })
101
+ },
102
+
103
+ /**
104
+ * Mocks the `organizations.create()` method.
105
+ * @param options Optional response customization.
106
+ * @returns A stub reference that can be used to restore the original behavior.
107
+ */
108
+ create: (options?: MockResponseOptions): StubReference => {
109
+ return this.createStub('organizations', 'create', () => {
110
+ const defaultData = this.generateOrganization()
111
+ if (options?.status !== undefined && options.status !== 200) {
112
+ throw new Exception('Mocked error', { status: options.status })
113
+ }
114
+ return (options?.data ?? defaultData) as unknown
115
+ })
116
+ },
117
+
118
+ invitations: {
119
+ list: (options?: MockResponseOptions): StubReference => {
120
+ return this.createStub('organizations.invitations', 'list', () => {
121
+ const defaultData = {
122
+ items: [this.generateInvitation(), this.generateInvitation()],
123
+ }
124
+ if (options?.status !== undefined && options.status !== 200) {
125
+ throw new Exception('Mocked error', { status: options.status })
126
+ }
127
+ return (options?.data ?? defaultData) as unknown
128
+ })
129
+ },
130
+ create: (options?: MockResponseOptions): StubReference => {
131
+ return this.createStub('organizations.invitations', 'create', () => {
132
+ const defaultData = this.generateInvitation()
133
+ if (options?.status !== undefined && options.status !== 200) {
134
+ throw new Exception('Mocked error', { status: options.status })
135
+ }
136
+ return (options?.data ?? defaultData) as unknown
137
+ })
138
+ },
139
+ findByToken: (options?: MockResponseOptions): StubReference => {
140
+ return this.createStub('organizations.invitations', 'findByToken', () => {
141
+ const defaultData = this.generateInvitation()
142
+ if (options?.status !== undefined && options.status !== 200) {
143
+ throw new Exception('Mocked error', { status: options.status })
144
+ }
145
+ return (options?.data ?? defaultData) as unknown
146
+ })
147
+ },
148
+ decline: (options?: MockResponseOptions): StubReference => {
149
+ return this.createStub('organizations.invitations', 'decline', () => {
150
+ const defaultData = this.generateInvitation()
151
+ if (options?.status !== undefined && options.status !== 200) {
152
+ throw new Exception('Mocked error', { status: options.status })
153
+ }
154
+ return (options?.data ?? defaultData) as unknown
155
+ })
156
+ },
157
+ delete: (options?: MockResponseOptions): StubReference => {
158
+ return this.createStub('organizations.invitations', 'delete', () => {
159
+ const defaultData = this.generateInvitation()
160
+ if (options?.status !== undefined && options.status !== 200) {
161
+ throw new Exception('Mocked error', { status: options.status })
162
+ }
163
+ return (options?.data ?? defaultData) as unknown
164
+ })
165
+ },
166
+ patch: (options?: MockResponseOptions): StubReference => {
167
+ return this.createStub('organizations.invitations', 'patch', () => {
168
+ const defaultData = this.generateInvitation()
169
+ if (options?.status !== undefined && options.status !== 200) {
170
+ throw new Exception('Mocked error', { status: options.status })
171
+ }
172
+ return (options?.data ?? defaultData) as unknown
173
+ })
174
+ },
175
+ resend: (options?: MockResponseOptions): StubReference => {
176
+ return this.createStub('organizations.invitations', 'resend', () => {
177
+ const defaultData = this.generateInvitation()
178
+ if (options?.status !== undefined && options.status !== 200) {
179
+ throw new Exception('Mocked error', { status: options.status })
180
+ }
181
+ return (options?.data ?? defaultData) as unknown
182
+ })
183
+ },
184
+ },
185
+
186
+ users: {
187
+ list: (options?: MockResponseOptions): StubReference => {
188
+ return this.createStub('organizations.users', 'list', () => {
189
+ const defaultData = {
190
+ items: [this.generateUser(), this.generateUser()],
191
+ }
192
+ if (options?.status !== undefined && options.status !== 200) {
193
+ throw new Exception('Mocked error', { status: options.status })
194
+ }
195
+ return (options?.data ?? defaultData) as unknown
196
+ })
197
+ },
198
+ read: (options?: MockResponseOptions): StubReference => {
199
+ return this.createStub('organizations.users', 'read', () => {
200
+ const defaultData = this.generateUser()
201
+ if (options?.status !== undefined && options.status !== 200) {
202
+ throw new Exception('Mocked error', { status: options.status })
203
+ }
204
+ return (options?.data ?? defaultData) as unknown
205
+ })
206
+ },
207
+ readBatch: (options?: MockResponseOptions): StubReference => {
208
+ return this.createStub('organizations.users', 'readBatch', () => {
209
+ const defaultData = {
210
+ items: [this.generateUser(), this.generateUser()],
211
+ }
212
+ if (options?.status !== undefined && options.status !== 200) {
213
+ throw new Exception('Mocked error', { status: options.status })
214
+ }
215
+ return (options?.data ?? defaultData) as unknown
216
+ })
217
+ },
218
+ activate: (options?: MockResponseOptions): StubReference => {
219
+ return this.createStub('organizations.users', 'activate', () => {
220
+ const defaultData = this.generateUser()
221
+ if (options?.status !== undefined && options.status !== 200) {
222
+ throw new Exception('Mocked error', { status: options.status })
223
+ }
224
+ return (options?.data ?? defaultData) as unknown
225
+ })
226
+ },
227
+ deactivate: (options?: MockResponseOptions): StubReference => {
228
+ return this.createStub('organizations.users', 'deactivate', () => {
229
+ const defaultData = this.generateUser()
230
+ if (options?.status !== undefined && options.status !== 200) {
231
+ throw new Exception('Mocked error', { status: options.status })
232
+ }
233
+ return (options?.data ?? defaultData) as unknown
234
+ })
235
+ },
236
+ delete: (options?: MockResponseOptions): StubReference => {
237
+ return this.createStub('organizations.users', 'delete', () => {
238
+ const status = options?.status ?? 204
239
+ if (status !== 204) {
240
+ throw new Exception('Mocked error', { status })
241
+ }
242
+ return undefined
243
+ })
244
+ },
245
+ },
246
+ }
247
+
248
+ /**
249
+ * Group API mocks.
250
+ */
251
+ groups = {
252
+ /**
253
+ * Mocks the `groups.list()` method.
254
+ * @param options Optional response customization.
255
+ * @returns A stub reference that can be used to restore the original behavior.
256
+ */
257
+ list: (options?: MockResponseOptions): StubReference => {
258
+ return this.createStub('groups', 'list', () => {
259
+ const defaultData = {
260
+ items: [this.generateGroup(), this.generateGroup()],
261
+ nextPageToken: undefined,
262
+ }
263
+ if (options?.status !== undefined && options.status !== 200) {
264
+ throw new Exception('Mocked error', { status: options.status })
265
+ }
266
+ return (options?.data ?? defaultData) as unknown
267
+ })
268
+ },
269
+
270
+ /**
271
+ * Mocks the `groups.create()` method.
272
+ * @param options Optional response customization.
273
+ * @returns A stub reference that can be used to restore the original behavior.
274
+ */
275
+ create: (options?: MockResponseOptions): StubReference => {
276
+ return this.createStub('groups', 'create', () => {
277
+ const defaultData = this.generateGroup()
278
+ const status = options?.status ?? 201
279
+ if (status !== 201) {
280
+ throw new Exception('Mocked error', { status })
281
+ }
282
+ return (options?.data ?? defaultData) as unknown
283
+ })
284
+ },
285
+
286
+ /**
287
+ * Mocks the `groups.read()` method.
288
+ * @param options Optional response customization.
289
+ * @returns A stub reference that can be used to restore the original behavior.
290
+ */
291
+ read: (options?: MockResponseOptions): StubReference => {
292
+ return this.createStub('groups', 'read', () => {
293
+ const defaultData = this.generateGroup()
294
+ if (options?.status !== undefined && options.status !== 200) {
295
+ throw new Exception('Mocked error', { status: options.status })
296
+ }
297
+ return (options?.data ?? defaultData) as unknown
298
+ })
299
+ },
300
+
301
+ /**
302
+ * Mocks the `groups.update()` method.
303
+ * @param options Optional response customization.
304
+ * @returns A stub reference that can be used to restore the original behavior.
305
+ */
306
+ update: (options?: MockResponseOptions): StubReference => {
307
+ return this.createStub('groups', 'update', () => {
308
+ const defaultData = this.generateGroup()
309
+ if (options?.status !== undefined && options.status !== 200) {
310
+ throw new Exception('Mocked error', { status: options.status })
311
+ }
312
+ return (options?.data ?? defaultData) as unknown
313
+ })
314
+ },
315
+
316
+ /**
317
+ * Mocks the `groups.delete()` method.
318
+ * @param options Optional response customization.
319
+ * @returns A stub reference that can be used to restore the original behavior.
320
+ */
321
+ delete: (options?: MockResponseOptions): StubReference => {
322
+ return this.createStub('groups', 'delete', () => {
323
+ const status = options?.status ?? 204
324
+ if (status !== 204) {
325
+ throw new Exception('Mocked error', { status })
326
+ }
327
+ return undefined
328
+ })
329
+ },
330
+
331
+ /**
332
+ * Mocks the `groups.addUsers()` method.
333
+ */
334
+ addUsers: (options?: MockResponseOptions): StubReference => {
335
+ return this.createStub('groups', 'addUsers', () => {
336
+ const defaultData = this.generateGroup()
337
+ if (options?.status !== undefined && options.status !== 200) {
338
+ throw new Exception('Mocked error', { status: options.status })
339
+ }
340
+ return (options?.data ?? defaultData) as unknown
341
+ })
342
+ },
343
+
344
+ /**
345
+ * Mocks the `groups.removeUsers()` method.
346
+ */
347
+ removeUsers: (options?: MockResponseOptions): StubReference => {
348
+ return this.createStub('groups', 'removeUsers', () => {
349
+ const defaultData = this.generateGroup()
350
+ if (options?.status !== undefined && options.status !== 200) {
351
+ throw new Exception('Mocked error', { status: options.status })
352
+ }
353
+ return (options?.data ?? defaultData) as unknown
354
+ })
355
+ },
356
+ }
357
+
358
+ /**
359
+ * User API mocks.
360
+ */
361
+ users = {
362
+ /**
363
+ * Mocks the `user.me()` method.
364
+ * @param options Optional response customization.
365
+ * @returns A stub reference that can be used to restore the original behavior.
366
+ */
367
+ me: (options?: MockResponseOptions): StubReference => {
368
+ return this.createStub('user', 'me', () => {
369
+ const defaultData = this.generateUser()
370
+ if (options?.status !== undefined && options.status !== 200) {
371
+ throw new Exception('Mocked error', { status: options.status })
372
+ }
373
+ return (options?.data ?? defaultData) as unknown
374
+ })
375
+ },
376
+ }
377
+
378
+ /**
379
+ * Auth API mocks.
380
+ */
381
+ auth = {
382
+ /**
383
+ * Mocks the `auth.oauthRedirect()` method.
384
+ * This method returns `null` by default as it performs window navigation.
385
+ * @param options Optional response customization.
386
+ * @returns A stub reference that can be used to restore the original behavior.
387
+ */
388
+ oauthRedirect: (options?: MockResponseOptions): StubReference => {
389
+ return this.createStub(
390
+ 'auth',
391
+ 'oauthRedirect',
392
+ () => {
393
+ const defaultData = options?.data ?? null
394
+ return defaultData
395
+ },
396
+ true
397
+ )
398
+ },
399
+ }
400
+
401
+ /**
402
+ * Files API mocks.
403
+ */
404
+ file = {
405
+ /**
406
+ * Mocks the `file.list()` method.
407
+ */
408
+ list: (options?: MockResponseOptions): StubReference => {
409
+ return this.createStub('file', 'list', () => {
410
+ const defaultData: ContextListResult<IFile> = {
411
+ items: [this.generateFile(), this.generateFile()],
412
+ }
413
+ if (options?.status !== undefined && options.status !== 200) {
414
+ throw new Exception('Mocked error', { status: options.status })
415
+ }
416
+ return (options?.data ?? defaultData) as unknown
417
+ })
418
+ },
419
+
420
+ /**
421
+ * Mocks the `file.createMeta()` method.
422
+ */
423
+ createMeta: (options?: MockResponseOptions): StubReference => {
424
+ return this.createStub('file', 'createMeta', () => {
425
+ const defaultData: IFile = this.generateFile()
426
+ const status = options?.status ?? 201
427
+ if (status !== 201) {
428
+ throw new Exception('Mocked error', { status })
429
+ }
430
+ return (options?.data ?? defaultData) as unknown
431
+ })
432
+ },
433
+
434
+ /**
435
+ * Mocks the `file.createMedia()` method.
436
+ */
437
+ createMedia: (options?: MockResponseOptions): StubReference => {
438
+ return this.createStub('file', 'createMedia', () => {
439
+ const status = options?.status ?? 200
440
+ if (status !== 200) {
441
+ throw new Exception('Mocked error', { status })
442
+ }
443
+ return undefined
444
+ })
445
+ },
446
+
447
+ /**
448
+ * Mocks the `file.create()` method.
449
+ */
450
+ create: (options?: MockResponseOptions): StubReference => {
451
+ return this.createStub('file', 'create', () => {
452
+ const status = options?.status ?? 201
453
+ if (status !== 201) {
454
+ throw new Exception('Mocked error', { status })
455
+ }
456
+ const defaultData: IFile = this.generateFile()
457
+ return (options?.data ?? defaultData) as unknown
458
+ })
459
+ },
460
+
461
+ /**
462
+ * Mocks the `file.createFolder()` method.
463
+ */
464
+ createFolder: (options?: MockResponseOptions): StubReference => {
465
+ return this.createStub('file', 'createFolder', () => {
466
+ const status = options?.status ?? 201
467
+ if (status !== 201) {
468
+ throw new Exception('Mocked error', { status })
469
+ }
470
+ const defaultData: IFolder = this.generateFolder()
471
+ return (options?.data ?? defaultData) as unknown
472
+ })
473
+ },
474
+
475
+ /**
476
+ * Mocks the `file.read()` method.
477
+ */
478
+ read: (options?: MockResponseOptions): StubReference => {
479
+ return this.createStub('file', 'read', () => {
480
+ const defaultData: IFile = this.generateFile()
481
+ if (options?.status !== undefined && options.status !== 200) {
482
+ throw new Exception('Mocked error', { status: options.status })
483
+ }
484
+ return (options?.data ?? defaultData) as unknown
485
+ })
486
+ },
487
+
488
+ /**
489
+ * Mocks the `file.readMedia()` method.
490
+ */
491
+ readMedia: (options?: MockResponseOptions): StubReference => {
492
+ return this.createStub('file', 'readMedia', () => {
493
+ if (options?.status !== undefined && options.status !== 200) {
494
+ throw new Exception('Mocked error', { status: options.status })
495
+ }
496
+ const defaultData = { media: { ok: true }, version: 1 }
497
+ return (options?.data ?? defaultData) as unknown
498
+ })
499
+ },
500
+
501
+ /**
502
+ * Mocks the `file.readBulk()` method.
503
+ */
504
+ readBulk: (options?: MockResponseOptions): StubReference => {
505
+ return this.createStub('file', 'readBulk', () => {
506
+ const defaultData: IBulkOperationResult<IFile> = {
507
+ items: [this.generateFile(), undefined],
508
+ }
509
+ if (options?.status !== undefined && options.status !== 200) {
510
+ throw new Exception('Mocked error', { status: options.status })
511
+ }
512
+ return (options?.data ?? defaultData) as unknown
513
+ })
514
+ },
515
+
516
+ /**
517
+ * Mocks the `file.patch()` method.
518
+ */
519
+ patch: (options?: MockResponseOptions): StubReference => {
520
+ return this.createStub('file', 'patch', () => {
521
+ const defaultData: IFile = this.generateFile()
522
+ if (options?.status !== undefined && options.status !== 200) {
523
+ throw new Exception('Mocked error', { status: options.status })
524
+ }
525
+ return (options?.data ?? defaultData) as unknown
526
+ })
527
+ },
528
+
529
+ /**
530
+ * Mocks the `file.patchMedia()` method.
531
+ */
532
+ patchMedia: (options?: MockResponseOptions): StubReference => {
533
+ return this.createStub('file', 'patchMedia', () => {
534
+ const defaultData: MediaPatchRevision = this.generateMediaPatchRevision()
535
+ if (options?.status !== undefined && options.status !== 200) {
536
+ throw new Exception('Mocked error', { status: options.status })
537
+ }
538
+ return (options?.data ?? defaultData) as unknown
539
+ })
540
+ },
541
+
542
+ /**
543
+ * Mocks the `file.delete()` method.
544
+ */
545
+ delete: (options?: MockResponseOptions): StubReference => {
546
+ return this.createStub('file', 'delete', () => {
547
+ const status = options?.status ?? 204
548
+ if (status !== 204) {
549
+ throw new Exception('Mocked error', { status })
550
+ }
551
+ return undefined
552
+ })
553
+ },
554
+
555
+ /**
556
+ * Mocks the `file.deleteBulk()` method.
557
+ */
558
+ deleteBulk: (options?: MockResponseOptions): StubReference => {
559
+ return this.createStub('file', 'deleteBulk', () => {
560
+ const status = options?.status ?? 204
561
+ if (status !== 204) {
562
+ throw new Exception('Mocked error', { status })
563
+ }
564
+ return undefined
565
+ })
566
+ },
567
+
568
+ /**
569
+ * Mocks the `file.patchUsers()` method.
570
+ */
571
+ patchUsers: (options?: MockResponseOptions): StubReference => {
572
+ return this.createStub('file', 'patchUsers', () => {
573
+ const defaultData: IFile = this.generateFile()
574
+ if (options?.status !== undefined && options.status !== 200) {
575
+ throw new Exception('Mocked error', { status: options.status })
576
+ }
577
+ return (options?.data ?? defaultData) as unknown
578
+ })
579
+ },
580
+
581
+ /**
582
+ * Mocks the `file.addUser()` method.
583
+ */
584
+ addUser: (options?: MockResponseOptions): StubReference => {
585
+ return this.createStub('file', 'addUser', () => {
586
+ const defaultData: IFile = this.generateFile()
587
+ if (options?.status !== undefined && options.status !== 200) {
588
+ throw new Exception('Mocked error', { status: options.status })
589
+ }
590
+ return (options?.data ?? defaultData) as unknown
591
+ })
592
+ },
593
+
594
+ /**
595
+ * Mocks the `file.removeUser()` method.
596
+ */
597
+ removeUser: (options?: MockResponseOptions): StubReference => {
598
+ return this.createStub('file', 'removeUser', () => {
599
+ const defaultData: IFile = this.generateFile()
600
+ if (options?.status !== undefined && options.status !== 200) {
601
+ throw new Exception('Mocked error', { status: options.status })
602
+ }
603
+ return (options?.data ?? defaultData) as unknown
604
+ })
605
+ },
606
+
607
+ /**
608
+ * Mocks the `file.listUsers()` method.
609
+ */
610
+ listUsers: (options?: MockResponseOptions): StubReference => {
611
+ return this.createStub('file', 'listUsers', () => {
612
+ const defaultData: ContextListResult<IUser> = {
613
+ items: [this.generateUser(), this.generateUser()],
614
+ }
615
+ if (options?.status !== undefined && options.status !== 200) {
616
+ throw new Exception('Mocked error', { status: options.status })
617
+ }
618
+ return (options?.data ?? defaultData) as unknown
619
+ })
620
+ },
621
+
622
+ /**
623
+ * Mocks the `file.breadcrumbs()` method.
624
+ */
625
+ breadcrumbs: (options?: MockResponseOptions): StubReference => {
626
+ return this.createStub('file', 'breadcrumbs', () => {
627
+ const defaultData: ContextListResult<FileBreadcrumb> = {
628
+ items: this.generateBreadcrumbs(),
629
+ }
630
+ if (options?.status !== undefined && options.status !== 200) {
631
+ throw new Exception('Mocked error', { status: options.status })
632
+ }
633
+ return (options?.data ?? defaultData) as unknown
634
+ })
635
+ },
636
+ }
637
+
638
+ /**
639
+ * Shared API mocks.
640
+ */
641
+ shared = {
642
+ list: (options?: MockResponseOptions): StubReference => {
643
+ return this.createStub('shared', 'list', () => {
644
+ const defaultData: ContextListResult<IFile> = {
645
+ items: [this.generateFile(), this.generateFile()],
646
+ }
647
+ if (options?.status !== undefined && options.status !== 200) {
648
+ throw new Exception('Mocked error', { status: options.status })
649
+ }
650
+ return (options?.data ?? defaultData) as unknown
651
+ })
652
+ },
653
+ }
654
+
655
+ /**
656
+ * Trash API mocks.
657
+ */
658
+ trash = {
659
+ list: (options?: MockResponseOptions): StubReference => {
660
+ return this.createStub('trash', 'list', () => {
661
+ const defaultData: ContextListResult<TrashEntry> = {
662
+ items: [this.generateTrashEntry(), this.generateTrashEntry()],
663
+ }
664
+ if (options?.status !== undefined && options.status !== 200) {
665
+ throw new Exception('Mocked error', { status: options.status })
666
+ }
667
+ return (options?.data ?? defaultData) as unknown
668
+ })
669
+ },
670
+ delete: (options?: MockResponseOptions): StubReference => {
671
+ return this.createStub('trash', 'delete', () => {
672
+ const status = options?.status ?? 204
673
+ if (status !== 204) {
674
+ throw new Exception('Mocked error', { status })
675
+ }
676
+ return undefined
677
+ })
678
+ },
679
+ restore: (options?: MockResponseOptions): StubReference => {
680
+ return this.createStub('trash', 'restore', () => {
681
+ const status = options?.status ?? 204
682
+ if (status !== 204) {
683
+ throw new Exception('Mocked error', { status })
684
+ }
685
+ return undefined
686
+ })
687
+ },
688
+ empty: (options?: MockResponseOptions): StubReference => {
689
+ return this.createStub('trash', 'empty', () => {
690
+ const status = options?.status ?? 204
691
+ if (status !== 204) {
692
+ throw new Exception('Mocked error', { status })
693
+ }
694
+ return undefined
695
+ })
696
+ },
697
+ }
698
+
699
+ /**
700
+ * Restores all stubs created by this mocker.
701
+ */
702
+ restore(): void {
703
+ this.stubs.forEach((stub) => stub.restore())
704
+ this.stubs = []
705
+ }
706
+
707
+ /**
708
+ * Creates a stub for a specific SDK method.
709
+ * @param api The API name (e.g., 'organizations', 'groups', 'user', 'auth').
710
+ * @param method The method name to stub.
711
+ * @param implementation The stub implementation that returns the mocked data.
712
+ * @returns A stub reference.
713
+ */
714
+ private createStub(
715
+ api:
716
+ | 'organizations'
717
+ | 'groups'
718
+ | 'user'
719
+ | 'auth'
720
+ | 'file'
721
+ | 'shared'
722
+ | 'trash'
723
+ | 'organizations.invitations'
724
+ | 'organizations.users',
725
+ method: string,
726
+ implementation: () => unknown,
727
+ sync?: boolean
728
+ ): StubReference {
729
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
730
+ let target: any = this.sdk
731
+ const parts = api.split('.')
732
+ for (const part of parts) {
733
+ target = target[part]
734
+ if (!target) {
735
+ throw new Error(`API '${api}' not found in SDK`)
736
+ }
737
+ }
738
+
739
+ const original = target[method]
740
+ if (typeof original !== 'function') {
741
+ throw new Error(`Method '${method}' not found in ${api} API`)
742
+ }
743
+
744
+ // Store original and create stub using sinon
745
+ const stub = sync
746
+ ? sinon.stub(target, method).callsFake(() => implementation())
747
+ : sinon.stub(target, method).callsFake(async () => implementation())
748
+
749
+ const stubRef: StubReference = {
750
+ restore: () => {
751
+ if (stub) {
752
+ stub.restore()
753
+ }
754
+ const index = this.stubs.indexOf(stubRef)
755
+ if (index > -1) {
756
+ this.stubs.splice(index, 1)
757
+ }
758
+ },
759
+ }
760
+
761
+ this.stubs.push(stubRef)
762
+ return stubRef
763
+ }
764
+
765
+ /**
766
+ * Creates a response object from data and options.
767
+ * @param defaultData Default data to return if not overridden.
768
+ * @param options Response options.
769
+ * @returns The final data to return (for non-HTTP responses) or IStoreResponse (for HTTP responses).
770
+ */
771
+ private createResponse(defaultData: unknown, options?: MockResponseOptions): unknown {
772
+ // For SDK-level stubs we return plain data. Status and headers are
773
+ // accepted but not propagated, since high-level SDK methods return data.
774
+ const data = options?.data !== undefined ? options.data : defaultData
775
+ return data
776
+ }
777
+
778
+ /**
779
+ * Generates a random organization object.
780
+ */
781
+ private generateOrganization(): IOrganization {
782
+ return {
783
+ kind: OrganizationKind,
784
+ key: nanoid(),
785
+ name: `Organization ${this.randomString()}`,
786
+ createdBy: nanoid(),
787
+ createdDate: Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 30,
788
+ grantType: this.randomChoice(['owner', 'manager', 'editor', 'viewer'] as const),
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Generates a random group object.
794
+ */
795
+ private generateGroup(): GroupSchema {
796
+ return {
797
+ kind: GroupKind,
798
+ key: nanoid(),
799
+ name: `Group ${this.randomString()}`,
800
+ description: `Description for ${this.randomString()}`,
801
+ owner: nanoid(),
802
+ oid: nanoid(),
803
+ users: [],
804
+ createdAt: Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 30,
805
+ updatedAt: Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 7,
806
+ icon: `https://example.com/icon-${nanoid()}.png`,
807
+ color: this.randomColor(),
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Generates a random user object.
813
+ */
814
+ private generateUser(): IUser {
815
+ const firstName = this.randomString()
816
+ const lastName = this.randomString()
817
+ return {
818
+ kind: UserKind,
819
+ key: nanoid(),
820
+ name: `${firstName} ${lastName}`,
821
+ email: [
822
+ {
823
+ email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,
824
+ verified: true,
825
+ },
826
+ ],
827
+ status: 'active',
828
+ created: Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 365,
829
+ updated: Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 7,
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Generates a random file meta object.
835
+ */
836
+ private generateFile(): IFile {
837
+ const kind = this.randomChoice([ProjectKind, DomainFileKind, CertificateFileKind, FolderKind] as const)
838
+ const name = `File ${this.randomString()}`
839
+ return File.createSchema({ kind, info: { name } })
840
+ }
841
+
842
+ /**
843
+ * Generates a random folder meta object.
844
+ */
845
+ private generateFolder(): IFolder {
846
+ const file = File.createSchema({ kind: FolderKind, info: { name: `Folder ${this.randomString()}` } }) as IFolder
847
+ return file
848
+ }
849
+
850
+ /**
851
+ * Generates a random media patch revision object.
852
+ */
853
+ private generateMediaPatchRevision(): MediaPatchRevision {
854
+ const version = Math.floor(Math.random() * 10) + 1
855
+ return {
856
+ id: nanoid(),
857
+ timestamp: Date.now(),
858
+ patch: [],
859
+ version,
860
+ revert: [],
861
+ newVersion: version + 1,
862
+ }
863
+ }
864
+
865
+ /**
866
+ * Generates a random breadcrumbs list.
867
+ */
868
+ private generateBreadcrumbs(): FileBreadcrumb[] {
869
+ const depth = 2 + Math.floor(Math.random() * 2) // 2-3
870
+ const items: FileBreadcrumb[] = []
871
+ for (let i = 0; i < depth; i += 1) {
872
+ items.push({ key: nanoid(), kind: i === depth - 1 ? ProjectKind : FolderKind, name: this.randomString() })
873
+ }
874
+ return items
875
+ }
876
+
877
+ /**
878
+ * Generates a random invitation object.
879
+ */
880
+ private generateInvitation(): InvitationSchema {
881
+ const firstName = this.randomString()
882
+ const lastName = this.randomString()
883
+ const now = Date.now()
884
+ return {
885
+ kind: InvitationKind,
886
+ key: nanoid(),
887
+ uid: nanoid(),
888
+ oid: nanoid(),
889
+ email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,
890
+ name: `${firstName} ${lastName}`,
891
+ token: nanoid(),
892
+ expiresAt: now + 7 * 24 * 60 * 60 * 1000,
893
+ status: 'pending',
894
+ grantType: this.randomChoice(['owner', 'manager', 'editor', 'viewer'] as const),
895
+ createdAt: now - Math.random() * 1000 * 60 * 60 * 24,
896
+ updatedAt: now - Math.random() * 1000 * 60 * 60,
897
+ resent: 0,
898
+ lastSentAt: now - Math.random() * 1000 * 60 * 60,
899
+ }
900
+ }
901
+
902
+ /**
903
+ * Generates a random trash entry.
904
+ */
905
+ private generateTrashEntry(): TrashEntry {
906
+ return {
907
+ key: nanoid(),
908
+ refKey: nanoid(),
909
+ kind: this.randomChoice([ProjectKind, DomainFileKind, CertificateFileKind, FolderKind] as const),
910
+ name: `Deleted ${this.randomString()}`,
911
+ info: { byMe: false, time: Date.now() - Math.floor(Math.random() * 1000000), user: nanoid(), name: 'User' },
912
+ capabilities: { canDelete: true, canRestore: true },
913
+ }
914
+ }
915
+
916
+ /**
917
+ * Generates a random string.
918
+ */
919
+ private randomString(): string {
920
+ const words = [
921
+ 'Alpha',
922
+ 'Beta',
923
+ 'Gamma',
924
+ 'Delta',
925
+ 'Epsilon',
926
+ 'Zeta',
927
+ 'Theta',
928
+ 'Lambda',
929
+ 'Sigma',
930
+ 'Omega',
931
+ 'Phoenix',
932
+ 'Dragon',
933
+ 'Tiger',
934
+ 'Eagle',
935
+ 'Falcon',
936
+ ]
937
+ return words[Math.floor(Math.random() * words.length)]
938
+ }
939
+
940
+ /**
941
+ * Returns a random choice from an array.
942
+ */
943
+ private randomChoice<T>(choices: readonly T[]): T {
944
+ return choices[Math.floor(Math.random() * choices.length)]
945
+ }
946
+
947
+ /**
948
+ * Generates a random hex color.
949
+ */
950
+ private randomColor(): string {
951
+ return `#${Math.floor(Math.random() * 16777215)
952
+ .toString(16)
953
+ .padStart(6, '0')}`
954
+ }
955
+ }