1- import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest'
1+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
2+
23import { SupabaseSetupClient } from '../../supabase/supabase'
34
45describe ( 'SupabaseDeployClient' , ( ) => {
@@ -655,9 +656,6 @@ describe('SupabaseDeployClient', () => {
655656 } )
656657
657658 // Mock only what we need to test
658- client . validateProject = vi
659- . fn ( )
660- . mockResolvedValue ( { id : mockProjectRef , name : 'test' , region : 'us-east-1' } )
661659 client . runSQL = vi . fn ( ) . mockResolvedValue ( null )
662660 client . deployFunction = vi . fn ( ) . mockResolvedValue ( null )
663661 client . setSecrets = mockSetSecrets
@@ -683,9 +681,6 @@ describe('SupabaseDeployClient', () => {
683681 } )
684682
685683 // Mock only what we need to test
686- client . validateProject = vi
687- . fn ( )
688- . mockResolvedValue ( { id : mockProjectRef , name : 'test' , region : 'us-east-1' } )
689684 client . runSQL = vi . fn ( ) . mockResolvedValue ( null )
690685 client . deployFunction = vi . fn ( ) . mockResolvedValue ( null )
691686 client . setSecrets = mockSetSecrets
@@ -701,4 +696,134 @@ describe('SupabaseDeployClient', () => {
701696 ] )
702697 } )
703698 } )
699+
700+ describe ( 'Setup secret authentication' , ( ) => {
701+ it ( 'passes the setup secret as bearer and the management token as x-management-api-token' , async ( ) => {
702+ const client = new SupabaseSetupClient ( {
703+ accessToken : mockAccessToken ,
704+ projectRef : mockProjectRef ,
705+ projectBaseUrl : 'test-domain.com' ,
706+ } )
707+
708+ const mockFetch = vi . fn ( ) . mockResolvedValue ( {
709+ ok : true ,
710+ json : async ( ) => ( { success : true } ) ,
711+ } )
712+ global . fetch = mockFetch
713+
714+ await client . invokeFunction ( 'stripe-setup' , 'DELETE' , 'the-setup-secret' , mockAccessToken )
715+
716+ expect ( mockFetch ) . toHaveBeenCalledWith (
717+ `https://${ mockProjectRef } .test-domain.com/functions/v1/stripe-setup` ,
718+ expect . objectContaining ( {
719+ method : 'DELETE' ,
720+ headers : expect . objectContaining ( {
721+ Authorization : 'Bearer the-setup-secret' ,
722+ 'x-management-api-token' : mockAccessToken ,
723+ } ) ,
724+ } )
725+ )
726+
727+ vi . restoreAllMocks ( )
728+ } )
729+
730+ it ( 'does not set x-management-api-token when no management token is provided' , async ( ) => {
731+ const client = new SupabaseSetupClient ( {
732+ accessToken : mockAccessToken ,
733+ projectRef : mockProjectRef ,
734+ } )
735+
736+ const mockFetch = vi . fn ( ) . mockResolvedValue ( {
737+ ok : true ,
738+ json : async ( ) => ( { success : true } ) ,
739+ } )
740+ global . fetch = mockFetch
741+
742+ await client . invokeFunction ( 'stripe-worker' , 'POST' , 'worker-secret' )
743+
744+ const headers = mockFetch . mock . calls [ 0 ] [ 1 ] . headers
745+ expect ( headers ) . not . toHaveProperty ( 'x-management-api-token' )
746+
747+ vi . restoreAllMocks ( )
748+ } )
749+
750+ it ( 'stores the setup secret in vault before invoking stripe-setup during install' , async ( ) => {
751+ const runSqlCalls : string [ ] = [ ]
752+ const client = new SupabaseSetupClient ( {
753+ accessToken : mockAccessToken ,
754+ projectRef : mockProjectRef ,
755+ } )
756+
757+ client . runSQL = vi . fn ( ) . mockImplementation ( async ( sql : string ) => {
758+ runSqlCalls . push ( sql )
759+ return null
760+ } )
761+ client . deployFunction = vi . fn ( ) . mockResolvedValue ( null )
762+ client . setSecrets = vi . fn ( ) . mockResolvedValue ( null )
763+ client . setupPgCronJob = vi . fn ( ) . mockResolvedValue ( null )
764+ client . setupSigmaPgCronJob = vi . fn ( ) . mockResolvedValue ( null )
765+
766+ let setupSecretStoredBeforeInvoke = false
767+ client . invokeFunction = vi . fn ( ) . mockImplementation ( async ( slug : string ) => {
768+ if ( slug === 'stripe-setup' ) {
769+ setupSecretStoredBeforeInvoke = runSqlCalls . some ( ( q ) =>
770+ q . includes ( "'stripe_setup_secret'" )
771+ )
772+ }
773+ return { success : true }
774+ } )
775+
776+ await client . install ( 'sk_test_key' )
777+
778+ expect ( setupSecretStoredBeforeInvoke ) . toBe ( true )
779+ } )
780+
781+ it ( 'authenticates stripe-setup with the secret read from vault, not the access token' , async ( ) => {
782+ const storedSecret = 'stored-setup-secret'
783+ const client = new SupabaseSetupClient ( {
784+ accessToken : mockAccessToken ,
785+ projectRef : mockProjectRef ,
786+ } )
787+
788+ client . runSQL = vi . fn ( ) . mockImplementation ( async ( sql : string ) => {
789+ if ( sql . includes ( 'stripe_setup_secret' ) ) {
790+ return [ { decrypted_secret : storedSecret } ]
791+ }
792+ if ( sql . includes ( 'schema_exists' ) ) {
793+ return [ { schema_exists : true } ]
794+ }
795+ return [ { comment : null } ]
796+ } )
797+
798+ const invokeSpy = vi . fn ( ) . mockResolvedValue ( { success : true } )
799+ client . invokeFunction = invokeSpy
800+ client . updateComment = vi . fn ( ) . mockResolvedValue ( undefined )
801+
802+ await client . uninstall ( )
803+
804+ expect ( invokeSpy ) . toHaveBeenCalledWith (
805+ 'stripe-setup' ,
806+ 'DELETE' ,
807+ storedSecret ,
808+ mockAccessToken
809+ )
810+ } )
811+
812+ it ( 'fails uninstall without invoking stripe-setup when the setup secret cannot be read' , async ( ) => {
813+ const client = new SupabaseSetupClient ( {
814+ accessToken : 'attacker-controlled-token' ,
815+ projectRef : mockProjectRef ,
816+ } )
817+
818+ // Simulate the Management API rejecting an unauthorized token: every
819+ // runSQL (including the vault read) throws.
820+ client . runSQL = vi . fn ( ) . mockRejectedValue ( new Error ( '401 Unauthorized' ) )
821+
822+ const invokeSpy = vi . fn ( ) . mockResolvedValue ( { success : true } )
823+ client . invokeFunction = invokeSpy
824+
825+ await expect ( client . uninstall ( ) ) . rejects . toThrow ( / S e t u p s e c r e t n o t f o u n d i n v a u l t / )
826+ expect ( invokeSpy ) . not . toHaveBeenCalled ( )
827+ } )
828+ } )
704829} )
0 commit comments