-
Notifications
You must be signed in to change notification settings - Fork 463
Expand file tree
/
Copy pathapi-keys-component.test.ts
More file actions
872 lines (709 loc) · 34.9 KB
/
Copy pathapi-keys-component.test.ts
File metadata and controls
872 lines (709 loc) · 34.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
import type { Application } from '../models/application';
import { appConfigs } from '../presets';
import type { FakeOrganization, FakeUser } from '../testUtils';
import { createTestUtils } from '../testUtils';
const mockAPIKeysEnvironmentSettings = async (
page: Page,
overrides: Partial<{
user_api_keys_enabled: boolean;
orgs_api_keys_enabled: boolean;
}>,
) => {
await page.route('*/**/v1/environment*', async route => {
const response = await route.fetch();
const json = await response.json();
const newJson = {
...json,
api_keys_settings: {
user_api_keys_enabled: true,
orgs_api_keys_enabled: true,
...overrides,
},
};
await route.fulfill({ response, json: newJson });
});
};
test.describe('api keys component @machine', () => {
test.describe.configure({ mode: 'serial' });
let app: Application;
let fakeAdmin: FakeUser;
let fakeOrganization: FakeOrganization;
test.beforeAll(async () => {
test.setTimeout(90_000); // Wait for app to be ready
app = await appConfigs.next.appRouter.clone().commit();
await app.setup();
await app.withEnv(appConfigs.envs.withAPIKeys);
await app.dev();
const u = createTestUtils({ app });
fakeAdmin = u.services.users.createFakeUser();
const admin = await u.services.users.createBapiUser(fakeAdmin);
fakeOrganization = await u.services.users.createFakeOrganization(admin.id);
});
test.afterAll(async () => {
await fakeOrganization.delete();
await fakeAdmin.deleteIfExists();
await app.teardown();
});
test('can create api keys', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
// Create API key 1
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-api-key-1`);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
// Close copy modal
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Create API key 2
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-api-key-2`);
await u.po.apiKeys.selectExpiration('7d');
await u.po.apiKeys.clickSaveButton();
// Wait and close copy modal
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Check if both API keys are created
await expect(u.page.locator('.cl-apiKeysTable .cl-tableBody .cl-tableRow')).toHaveCount(2);
});
test('pagination works correctly with multiple pages', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
// Create user and 11 API keys to trigger pagination (default perPage is 10)
const fakeUser = u.services.users.createFakeUser();
const bapiUser = await u.services.users.createBapiUser(fakeUser);
const fakeAPIKeys = await Promise.all(
Array.from({ length: 11 }, () => u.services.users.createFakeAPIKey(bapiUser.id)),
);
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
// Verify first page
await expect(u.page.getByText(/Displaying 1 – 10 of 11/i)).toBeVisible();
await expect(u.page.locator('.cl-apiKeysTable .cl-tableBody .cl-tableRow')).toHaveCount(10);
// Navigate to second page
const page2Button = u.page.locator('.cl-paginationButton').filter({ hasText: /^2$/ });
await page2Button.click();
await expect(u.page.getByText(/Displaying 11 – 11 of 11/i)).toBeVisible();
await expect(u.page.locator('.cl-apiKeysTable .cl-tableBody .cl-tableRow')).toHaveCount(1);
// Navigate back to first page
const page1Button = u.page.locator('.cl-paginationButton').filter({ hasText: /^1$/ });
await page1Button.click();
await expect(u.page.getByText(/Displaying 1 – 10 of 11/i)).toBeVisible();
await expect(u.page.locator('.cl-apiKeysTable .cl-tableBody .cl-tableRow')).toHaveCount(10);
// Cleanup
await Promise.all(fakeAPIKeys.map(key => key.revoke()));
await fakeUser.deleteIfExists();
});
test('pagination does not show when items fit in one page', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const apiKeyName = `${fakeAdmin.firstName}-single-page-${Date.now()}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(apiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
await expect(u.page.getByText(/Displaying.*of.*/i)).toBeHidden();
});
test('can revoke api keys', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const apiKeyName = `${fakeAdmin.firstName}-${Date.now()}`;
// Create API key
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(apiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
// Wait and close copy modal
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Retrieve API key
const table = u.page.locator('.cl-apiKeysTable');
const row = table.locator('.cl-tableRow', { hasText: apiKeyName });
await row.waitFor({ state: 'attached' });
// Revoke API key
await row.locator('.cl-menuButton').click();
const revokeButton = u.page.getByRole('menuitem', { name: 'Revoke key' });
await revokeButton.waitFor({ state: 'attached' });
await revokeButton.click();
// Wait for revoke modal and confirm revocation
await u.po.apiKeys.waitForRevokeModalOpened();
await u.po.apiKeys.typeRevokeConfirmation('Revoke');
await u.po.apiKeys.clickConfirmRevokeButton();
await u.po.apiKeys.waitForRevokeModalClosed();
// Check if record is removed from the table
await expect(table.locator('.cl-tableRow', { hasText: apiKeyName })).toHaveCount(0);
});
test('can copy api key secret after creation', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const apiKeyName = `${fakeAdmin.firstName}-${Date.now()}`;
// Create API key and capture the secret from the response
const createResponsePromise = page.waitForResponse(
response => response.url().includes('/api_keys') && response.request().method() === 'POST',
);
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(apiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
const createResponse = await createResponsePromise;
const secret = (await createResponse.json()).secret;
// Copy secret via modal and verify clipboard contents
// Wait and close copy modal
await u.po.apiKeys.waitForCopyModalOpened();
await context.grantPermissions(['clipboard-read']);
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
const clipboardText = await page.evaluate('navigator.clipboard.readText()');
await context.clearPermissions();
expect(clipboardText).toBe(secret);
});
test('UserProfile API keys page visibility', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
// user_api_keys_enabled: false should hide API keys page
await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: false });
await u.po.page.goToRelative('/user');
await u.po.userProfile.waitForMounted();
await u.po.page.goToRelative('/user#/api-keys');
await expect(u.page.locator('.cl-apiKeys')).toBeHidden({ timeout: 2000 });
// user_api_keys_enabled: true should show API keys page
await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: true });
await page.reload();
await u.po.userProfile.waitForMounted();
await u.po.page.goToRelative('/user#/api-keys');
await expect(u.page.locator('.cl-apiKeys')).toBeVisible({ timeout: 5000 });
await u.page.unrouteAll();
});
test('OrganizationProfile API keys page visibility', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
// orgs_api_keys_enabled: false should hide API keys page
await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: false });
await u.po.page.goToRelative('/organization-profile');
await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
await expect(u.page.locator('.cl-apiKeys')).toBeHidden({ timeout: 2000 });
// orgs_api_keys_enabled: true should show API keys page
await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: true });
await page.reload();
await u.po.page.goToRelative('/organization-profile#/organization-api-keys');
await expect(u.page.locator('.cl-apiKeys')).toBeVisible({ timeout: 5000 });
await u.page.unrouteAll();
});
test('UserProfile API keys uses user ID as subject even when organization is active', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
const admin = await u.services.users.getUser({ email: fakeAdmin.email });
expect(admin).toBeDefined();
const userId = admin.id;
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.organizationSwitcher.goTo();
await u.po.organizationSwitcher.waitForMounted();
await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
let capturedSubject: string | null = null;
const apiKeyRequestPromise = u.page.waitForRequest(request => {
if (request.url().includes('api_keys')) {
const url = new URL(request.url());
capturedSubject = url.searchParams.get('subject');
return true;
}
return false;
});
await u.po.page.goToRelative('/user');
await u.po.userProfile.waitForMounted();
await u.po.userProfile.switchToAPIKeysTab();
await apiKeyRequestPromise;
// Verify the subject parameter is the user ID, not the organization ID
expect(capturedSubject).toBe(userId);
expect(capturedSubject).not.toBe(fakeOrganization.organization.id);
});
test('standalone API keys component in user context based on user_api_keys_enabled', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
// user_api_keys_enabled: false should prevent standalone component from rendering
await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: false });
let apiKeysRequestWasMade = false;
await u.page.route('**/api_keys*', async route => {
apiKeysRequestWasMade = true;
await route.abort();
});
await u.po.page.goToRelative('/api-keys');
await expect(u.page.locator('.cl-apiKeys-root')).toBeHidden({ timeout: 1000 });
expect(apiKeysRequestWasMade).toBe(false);
// user_api_keys_enabled: true should allow standalone component to render
await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: true });
await page.reload();
await u.po.apiKeys.waitForMounted();
await expect(u.page.locator('.cl-apiKeys-root')).toBeVisible();
await u.page.unrouteAll();
});
test('standalone API keys component in org context based on orgs_api_keys_enabled', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
// orgs_api_keys_enabled: false should prevent standalone component from rendering in org context
await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: false });
let apiKeysRequestWasMade = false;
await u.page.route('**/api_keys*', async route => {
apiKeysRequestWasMade = true;
await route.abort();
});
await u.po.page.goToRelative('/api-keys');
await expect(u.page.locator('.cl-apiKeys-root')).toBeHidden({ timeout: 1000 });
expect(apiKeysRequestWasMade).toBe(false);
// orgs_api_keys_enabled: true should allow standalone component to render in org context
await mockAPIKeysEnvironmentSettings(u.page, { orgs_api_keys_enabled: true });
await page.reload();
await u.po.apiKeys.waitForMounted();
await expect(u.page.locator('.cl-apiKeys-root')).toBeVisible();
await u.page.unrouteAll();
});
test('standalone API keys component renders in org context when user API keys are disabled', async ({
page,
context,
}) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.organizationSwitcher.goTo();
await u.po.organizationSwitcher.waitForMounted();
await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
await mockAPIKeysEnvironmentSettings(u.page, { user_api_keys_enabled: false });
let capturedSubject: string | null = null;
const apiKeyRequestPromise = u.page.waitForRequest(request => {
if (request.url().includes('api_keys')) {
const url = new URL(request.url());
capturedSubject = url.searchParams.get('subject');
return true;
}
return false;
});
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
await expect(u.page.locator('.cl-apiKeys-root')).toBeVisible();
// Org API keys are listed, so the subject must be the organization
await apiKeyRequestPromise;
expect(capturedSubject).toBe(fakeOrganization.organization.id);
await u.page.unrouteAll();
});
test.describe('api key list invalidation', () => {
// Helper function to count actual API key rows (not empty state)
const createAPIKeyCountHelper = (u: any) => async () => {
// Wait for the table to be fully loaded first
await u.page.locator('.cl-apiKeysTable').waitFor({ timeout: 10000 });
// Wait for any ongoing navigation/pagination to complete
await u.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {
// Ignore timeout - continue with other checks
});
// Wait for content to stabilize - check multiple times to ensure consistency
let stableCount = -1;
let retryCount = 0;
const maxRetries = 10;
while (retryCount < maxRetries) {
// Wait for content to load (either empty state or actual data)
await u.page
.waitForFunction(
() => {
const emptyText = document.querySelector(
'text[data-localization-key*="emptyRow"], [data-localization-key*="emptyRow"]',
);
const menuButtons = document.querySelectorAll(
'.cl-apiKeysTable .cl-tableBody .cl-tableRow .cl-menuButton',
);
const spinner = document.querySelector('.cl-spinner');
// Content is loaded if we have either empty state, menu buttons, or no spinner
return emptyText || menuButtons.length > 0 || !spinner;
},
{ timeout: 3000 },
)
.catch(() => {
// Continue to next check if this fails
});
// Check if spinner is still visible (still loading)
const spinner = u.page.locator('.cl-spinner');
if (await spinner.isVisible().catch(() => false)) {
await spinner.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {
// Continue if spinner doesn't disappear
});
}
// Check for empty state first
const emptyStateText = await u.page
.getByText('No API keys found')
.isVisible()
.catch(() => false);
if (emptyStateText) {
return 0;
}
// Count menu buttons (each API key row has one)
const menuButtons = u.page.locator('.cl-apiKeysTable .cl-tableBody .cl-tableRow .cl-menuButton');
const currentCount = await menuButtons.count();
// Check if count has stabilized (same as previous check)
if (currentCount === stableCount) {
return currentCount;
}
stableCount = currentCount;
retryCount++;
// Small delay before next check to allow for DOM updates
if (retryCount < maxRetries) {
await u.page.waitForTimeout(200);
}
}
// Return the last stable count if we've exhausted retries
return stableCount;
};
test('api key list invalidation: new key appears immediately after creation', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const getAPIKeyCount = createAPIKeyCountHelper(u);
const initialRowCount = await getAPIKeyCount();
// Create a new API key with unique name
const newApiKeyName = `invalidation-test-${Date.now()}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(newApiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
// Close copy modal
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Verify the new API key appears in the list immediately (invalidation worked)
const table = u.page.locator('.cl-apiKeysTable');
await expect(table.locator('.cl-tableRow', { hasText: newApiKeyName })).toBeVisible({ timeout: 5000 });
// Verify the total count increased
const finalRowCount = await getAPIKeyCount();
expect(finalRowCount).toBe(initialRowCount + 1);
// Clean up - revoke the API key created in this test to avoid interfering with other tests
const menuButton = table.locator('.cl-tableRow', { hasText: newApiKeyName }).locator('.cl-menuButton');
await menuButton.click();
const revokeButton = u.page.getByRole('menuitem', { name: 'Revoke key' });
await revokeButton.click();
await u.po.apiKeys.waitForRevokeModalOpened();
await u.po.apiKeys.typeRevokeConfirmation('Revoke');
await u.po.apiKeys.clickConfirmRevokeButton();
await u.po.apiKeys.waitForRevokeModalClosed();
});
test('api key list invalidation: pagination info updates after creation', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
// Create a dedicated user for this test to ensure clean state
const dedicatedUser = u.services.users.createFakeUser();
const bapiUser = await u.services.users.createBapiUser(dedicatedUser);
// Create exactly 9 API keys for this user (not using shared organization)
const existingKeys = await Promise.all(
Array.from({ length: 9 }, () => u.services.users.createFakeAPIKey(bapiUser.id)),
);
// Sign in with the dedicated user
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({
email: dedicatedUser.email,
password: dedicatedUser.password,
});
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const getAPIKeyCount = createAPIKeyCountHelper(u);
// Verify we have 9 keys and no pagination (all fit in first page)
// The helper function already has robust waiting logic
const actualCount = await getAPIKeyCount();
expect(actualCount).toBe(9);
await expect(u.page.getByText(/Displaying.*of.*/i)).toBeHidden();
// Create the 10th API key which should not trigger pagination yet
const newApiKeyName = `boundary-test-${Date.now()}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(newApiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Verify we now have 10 keys and still no pagination (exactly fits in one page)
expect(await getAPIKeyCount()).toBe(10);
await expect(u.page.getByText(/Displaying.*of.*/i)).toBeHidden();
// Create the 11th API key which should trigger pagination
const eleventhKeyName = `pagination-trigger-${Date.now()}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(eleventhKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Verify pagination info appears and shows correct count (invalidation updated pagination)
await expect(u.page.getByText(/Displaying 1 – 10 of 11/i)).toBeVisible({ timeout: 5000 });
expect(await getAPIKeyCount()).toBe(10);
// Cleanup - revoke the API keys created for this test and delete the user
await Promise.all(existingKeys.map(key => key.revoke()));
await dedicatedUser.deleteIfExists();
});
test('api key list invalidation: works with active search filter', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const getAPIKeyCount = createAPIKeyCountHelper(u);
// Create a specific search term that will match our new key
const timestamp = Date.now();
const searchTerm = `searchfilter-${timestamp}`;
const newApiKeyName = `${searchTerm}-key`;
// Apply search filter first
const searchInput = u.page.locator('input.cl-apiKeysSearchInput');
await searchInput.fill(searchTerm);
// Wait for search to actually filter results - either empty state appears
// or the loading/fetching state completes with no matching results.
await expect(async () => {
const emptyMessage = u.page.locator('[data-localization-key*="emptyRow"]');
const isEmptyVisible = await emptyMessage.isVisible().catch(() => false);
expect(isEmptyVisible).toBe(true);
}).toPass({ timeout: 10000 });
// Verify no results initially match
expect(await getAPIKeyCount()).toBe(0);
// Create API key that matches the search
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(newApiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Verify the new key appears in filtered results (invalidation worked with search)
const table = u.page.locator('.cl-apiKeysTable');
await expect(table.locator('.cl-tableRow', { hasText: newApiKeyName })).toBeVisible({ timeout: 5000 });
expect(await getAPIKeyCount()).toBe(1);
// Clear search and verify key appears in full list too
await searchInput.clear();
// Wait for search to clear and show all results
await u.page.waitForFunction(
() => {
return document.querySelectorAll('.cl-apiKeysTable .cl-tableBody .cl-tableRow .cl-menuButton').length > 0;
},
{ timeout: 5000 },
);
await expect(table.locator('.cl-tableRow', { hasText: newApiKeyName })).toBeVisible();
});
test('api key list invalidation: works when on second page of results', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
// Create a dedicated user for this test to ensure clean state
const dedicatedUser = u.services.users.createFakeUser();
const bapiUser = await u.services.users.createBapiUser(dedicatedUser);
// Create exactly 15 API keys for this user to have 2 pages (10 per page)
const existingKeys = await Promise.all(
Array.from({ length: 15 }, () => u.services.users.createFakeAPIKey(bapiUser.id)),
);
// Sign in with the dedicated user
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({
email: dedicatedUser.email,
password: dedicatedUser.password,
});
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const getAPIKeyCount = createAPIKeyCountHelper(u);
// Verify pagination and go to second page
await expect(u.page.getByText(/Displaying 1 – 10 of 15/i)).toBeVisible();
const page2Button = u.page.locator('.cl-paginationButton').filter({ hasText: /^2$/ });
await page2Button.click();
await expect(u.page.getByText(/Displaying 11 – 15 of 15/i)).toBeVisible();
expect(await getAPIKeyCount()).toBe(5);
// Create a new API key while on page 2
const newApiKeyName = `page2-test-${Date.now()}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(newApiKeyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Verify pagination info updated (invalidation refreshed all pages)
await expect(u.page.getByText(/Displaying 11 – 16 of 16/i)).toBeVisible({ timeout: 5000 });
expect(await getAPIKeyCount()).toBe(6);
// The new key should appear on page 1 since it's the most recent
const table = u.page.locator('.cl-apiKeysTable');
await expect(table.locator('.cl-tableRow', { hasText: newApiKeyName })).toBeVisible();
// Cleanup - revoke the API keys created for this test and delete the user
await Promise.all(existingKeys.map(key => key.revoke()));
await dedicatedUser.deleteIfExists();
});
test('api key list invalidation: multiple rapid creations update correctly', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const getAPIKeyCount = createAPIKeyCountHelper(u);
const initialRowCount = await getAPIKeyCount();
const timestamp = Date.now();
// Create multiple API keys rapidly to test invalidation handles concurrent updates
for (let i = 0; i < 3; i++) {
const keyName = `rapid-test-${timestamp}-${i}`;
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(keyName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
}
// Verify all 3 new keys appear in the list
const table = u.page.locator('.cl-apiKeysTable');
for (let i = 0; i < 3; i++) {
const keyName = `rapid-test-${timestamp}-${i}`;
await expect(table.locator('.cl-tableRow', { hasText: keyName })).toBeVisible({ timeout: 5000 });
}
// Verify total count increased by 3
const finalRowCount = await getAPIKeyCount();
expect(finalRowCount).toBe(initialRowCount + 3);
// Clean up - revoke the API keys created in this test to avoid interfering with other tests
for (let i = 0; i < 3; i++) {
const keyName = `rapid-test-${timestamp}-${i}`;
const menuButton = table.locator('.cl-tableRow', { hasText: keyName }).locator('.cl-menuButton');
await menuButton.click();
const revokeButton = u.page.getByRole('menuitem', { name: 'Revoke key' });
await revokeButton.click();
await u.po.apiKeys.waitForRevokeModalOpened();
await u.po.apiKeys.typeRevokeConfirmation('Revoke');
await u.po.apiKeys.clickConfirmRevokeButton();
await u.po.apiKeys.waitForRevokeModalClosed();
}
});
});
test('shows error when creating API key with duplicate name', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
const duplicateName = `${fakeAdmin.firstName}-duplicate-${Date.now()}`;
// Create the first API key
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(duplicateName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
await u.po.apiKeys.waitForCopyModalOpened();
await u.po.apiKeys.clickCopyAndCloseButton();
await u.po.apiKeys.waitForCopyModalClosed();
await u.po.apiKeys.waitForFormClosed();
// Try to create another API key with the same name
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(duplicateName);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
// Verify error message is displayed
await expect(u.page.getByText('API Key name already exists.')).toBeVisible({ timeout: 5000 });
});
test('shows error when API key usage is exceeded for free plan', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
// Mock the API keys create endpoint to return 403 for free plan users who exceed free tier limits
await page.route('*/**/api_keys*', async route => {
if (route.request().method() === 'POST') {
await route.fulfill({
status: 403,
contentType: 'application/json',
body: JSON.stringify({
errors: [{ code: 'token_quota_exceeded', message: 'Token quota exceeded' }],
}),
});
} else {
await route.continue();
}
});
await u.po.page.goToRelative('/api-keys');
await u.po.apiKeys.waitForMounted();
await u.po.apiKeys.clickAddButton();
await u.po.apiKeys.waitForFormOpened();
await u.po.apiKeys.typeName(`${fakeAdmin.firstName}-test-usage-exceeded`);
await u.po.apiKeys.selectExpiration('1d');
await u.po.apiKeys.clickSaveButton();
// Verify error message is displayed
await expect(
u.page.getByText('You have reached your usage limit. You can remove the limit by upgrading to a paid plan.'),
).toBeVisible({ timeout: 5000 });
await u.page.unrouteAll();
});
});