@@ -64,7 +64,11 @@ vi.mock('@/lib/credentials/token-service-accounts/errors', () => ({
6464} ) )
6565vi . mock ( '@/lib/posthog/server' , ( ) => ( { captureServerEvent : vi . fn ( ) } ) )
6666
67- import { deleteCredentialRecord , performUpdateCredential } from '@/lib/credentials/orchestration'
67+ import {
68+ createServiceAccountCredential ,
69+ deleteCredentialRecord ,
70+ performUpdateCredential ,
71+ } from '@/lib/credentials/orchestration'
6872
6973const OLD_EMAIL = 'old-sa@old-project.iam.gserviceaccount.com'
7074const NEW_EMAIL = 'new-sa@new-project.iam.gserviceaccount.com'
@@ -434,6 +438,93 @@ describe('performUpdateCredential — service-account secret rotation', () => {
434438 } )
435439} )
436440
441+ describe ( 'createServiceAccountCredential' , ( ) => {
442+ beforeEach ( ( ) => {
443+ vi . clearAllMocks ( )
444+ resetDbChainMock ( )
445+ } )
446+
447+ it ( 'rejects an existing service-account source instead of discarding the submitted secret' , async ( ) => {
448+ mockVerifyAndBuildServiceAccountSecret . mockResolvedValue ( {
449+ providerId : 'zoom-service-account' ,
450+ encryptedServiceAccountKey : 'new-cipher' ,
451+ displayName : 'Production Zoom' ,
452+ auditMetadata : { } ,
453+ principal : { kind : 'tenant' , id : 'account-1' } ,
454+ } )
455+ queueTableRows ( schemaMock . credential , [
456+ {
457+ id : 'credential-1' ,
458+ workspaceId : 'workspace-1' ,
459+ type : 'service_account' ,
460+ providerId : 'zoom-service-account' ,
461+ displayName : 'Production Zoom' ,
462+ encryptedServiceAccountKey : 'old-cipher' ,
463+ } ,
464+ ] )
465+ mockDecryptSecret
466+ . mockResolvedValueOnce ( { decrypted : 'stored-secret' } )
467+ . mockResolvedValueOnce ( { decrypted : 'rotated-secret' } )
468+ mockGetCredentialActorContext . mockResolvedValue ( { member : { role : 'admin' } , isAdmin : true } )
469+
470+ const result = await createServiceAccountCredential ( {
471+ workspaceId : 'workspace-1' ,
472+ userId : 'user-1' ,
473+ providerId : 'zoom-service-account' ,
474+ displayName : 'Production Zoom' ,
475+ clientId : 'client-id' ,
476+ clientSecret : 'rotated-client-secret' ,
477+ orgId : 'account-1' ,
478+ } )
479+
480+ expect ( result ) . toMatchObject ( {
481+ success : false ,
482+ errorCode : 'conflict' ,
483+ providerErrorCode : 'duplicate_display_name' ,
484+ } )
485+ expect ( dbChainMockFns . insert ) . not . toHaveBeenCalled ( )
486+ expect ( mockGetCredentialActorContext ) . toHaveBeenCalledWith ( 'credential-1' , 'user-1' , { } )
487+ } )
488+
489+ it ( 'returns an accessible credential for an exact non-token secret replay' , async ( ) => {
490+ const existingCredential = {
491+ id : 'credential-1' ,
492+ workspaceId : 'workspace-1' ,
493+ type : 'service_account' ,
494+ providerId : 'zoom-service-account' ,
495+ displayName : 'Production Zoom' ,
496+ encryptedServiceAccountKey : 'stored-cipher' ,
497+ }
498+ mockVerifyAndBuildServiceAccountSecret . mockResolvedValue ( {
499+ providerId : 'zoom-service-account' ,
500+ encryptedServiceAccountKey : 'replay-cipher' ,
501+ displayName : 'Production Zoom' ,
502+ auditMetadata : { } ,
503+ principal : { kind : 'tenant' , id : 'account-1' } ,
504+ } )
505+ queueTableRows ( schemaMock . credential , [ existingCredential ] )
506+ mockDecryptSecret . mockResolvedValue ( { decrypted : 'same-secret' } )
507+ mockGetCredentialActorContext . mockResolvedValue ( { member : { role : 'admin' } , isAdmin : true } )
508+
509+ const result = await createServiceAccountCredential ( {
510+ workspaceId : 'workspace-1' ,
511+ userId : 'user-1' ,
512+ providerId : 'zoom-service-account' ,
513+ displayName : 'Production Zoom' ,
514+ clientId : 'client-id' ,
515+ clientSecret : 'client-secret' ,
516+ orgId : 'account-1' ,
517+ } )
518+
519+ expect ( result ) . toMatchObject ( {
520+ success : true ,
521+ credential : existingCredential ,
522+ created : false ,
523+ } )
524+ expect ( dbChainMockFns . insert ) . not . toHaveBeenCalled ( )
525+ } )
526+ } )
527+
437528describe ( 'deleteCredentialRecord' , ( ) => {
438529 beforeEach ( ( ) => {
439530 vi . clearAllMocks ( )
0 commit comments