@@ -20,6 +20,8 @@ import (
2020 mcpv1beta1 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1"
2121 "github.com/stacklok/toolhive/cmd/thv-operator/api/v1beta1/v1beta1test"
2222 "github.com/stacklok/toolhive/pkg/vmcp"
23+ authtypes "github.com/stacklok/toolhive/pkg/vmcp/auth/types"
24+ "github.com/stacklok/toolhive/pkg/vmcp/config"
2325 "github.com/stacklok/toolhive/pkg/vmcp/k8s"
2426 "github.com/stacklok/toolhive/pkg/vmcp/workloads"
2527)
@@ -645,3 +647,140 @@ func TestMapAuthConfigToEntries(t *testing.T) {
645647 })
646648 }
647649}
650+
651+ // newOutgoingAuthTestFixture creates the fake client, discoverer, and registry
652+ // shared by the outgoing-auth reconciliation tests below. The MCPServer carries
653+ // no ExternalAuthConfigRef, so any auth on the upserted backend must come from
654+ // the reconciler's OutgoingAuth config (or from discoveredAuth when non-nil,
655+ // simulating auth resolved from the resource's own references).
656+ func newOutgoingAuthTestFixture (
657+ t * testing.T ,
658+ discoveredAuth * authtypes.BackendAuthStrategy ,
659+ ) (client.Client , * mockDiscoverer , * mockRegistry ) {
660+ t .Helper ()
661+
662+ scheme := runtime .NewScheme ()
663+ require .NoError (t , mcpv1beta1 .AddToScheme (scheme ))
664+
665+ mcpServer := & mcpv1beta1.MCPServer {
666+ ObjectMeta : metav1.ObjectMeta {
667+ Name : "test-server" ,
668+ Namespace : "default" ,
669+ },
670+ Spec : mcpv1beta1.MCPServerSpec {
671+ GroupRef : & mcpv1beta1.MCPGroupRef {Name : "test-group" },
672+ },
673+ }
674+
675+ k8sClient := fake .NewClientBuilder ().
676+ WithScheme (scheme ).
677+ WithObjects (mcpServer ).
678+ Build ()
679+
680+ mockBackend := & vmcp.Backend {
681+ ID : "test-server" ,
682+ Name : "test-server" ,
683+ BaseURL : "http://test-server:8080" ,
684+ AuthConfig : discoveredAuth ,
685+ }
686+
687+ return k8sClient , & mockDiscoverer {backend : mockBackend }, & mockRegistry {}
688+ }
689+
690+ // reconcileTestServer runs one reconcile of the "test-server" MCPServer and
691+ // returns the single upserted backend.
692+ func reconcileTestServer (t * testing.T , reconciler * k8s.BackendReconciler , mockReg * mockRegistry ) vmcp.Backend {
693+ t .Helper ()
694+
695+ req := ctrl.Request {
696+ NamespacedName : types.NamespacedName {
697+ Name : "test-server" ,
698+ Namespace : "default" ,
699+ },
700+ }
701+
702+ result , err := reconciler .Reconcile (context .Background (), req )
703+ require .NoError (t , err )
704+ assert .Equal (t , ctrl.Result {}, result )
705+ require .Len (t , mockReg .upsertedBackends , 1 , "Backend should be upserted to registry" )
706+ return mockReg .upsertedBackends [0 ]
707+ }
708+
709+ // TestReconcile_OutgoingAuthBackendsEntry verifies that a config-level
710+ // outgoingAuth.backends.<name> entry survives reconciliation for a backend whose
711+ // own resource carries no discovered auth. Regression test for the watcher path
712+ // silently dropping config-level outgoing auth (#6454).
713+ func TestReconcile_OutgoingAuthBackendsEntry (t * testing.T ) {
714+ t .Parallel ()
715+
716+ k8sClient , mockDisc , mockReg := newOutgoingAuthTestFixture (t , nil )
717+
718+ wantStrategy := & authtypes.BackendAuthStrategy {Type : authtypes .StrategyTypeUpstreamInject }
719+ reconciler := newTestReconciler (k8sClient , "default" , "test-group" , mockReg , mockDisc )
720+ reconciler .OutgoingAuth = & config.OutgoingAuthConfig {
721+ Source : "discovered" ,
722+ Backends : map [string ]* authtypes.BackendAuthStrategy {
723+ "test-server" : wantStrategy ,
724+ },
725+ }
726+
727+ upserted := reconcileTestServer (t , reconciler , mockReg )
728+ require .NotNil (t , upserted .AuthConfig , "backends[<name>] auth config must survive reconciliation" )
729+ assert .Equal (t , authtypes .StrategyTypeUpstreamInject , upserted .AuthConfig .Type )
730+ }
731+
732+ // TestReconcile_OutgoingAuthDefault verifies that the config-level
733+ // outgoingAuth.default survives reconciliation for a backend whose own resource
734+ // carries no discovered auth. Regression test for #6454.
735+ func TestReconcile_OutgoingAuthDefault (t * testing.T ) {
736+ t .Parallel ()
737+
738+ k8sClient , mockDisc , mockReg := newOutgoingAuthTestFixture (t , nil )
739+
740+ reconciler := newTestReconciler (k8sClient , "default" , "test-group" , mockReg , mockDisc )
741+ reconciler .OutgoingAuth = & config.OutgoingAuthConfig {
742+ Source : "discovered" ,
743+ Default : & authtypes.BackendAuthStrategy {Type : authtypes .StrategyTypeHeaderInjection },
744+ }
745+
746+ upserted := reconcileTestServer (t , reconciler , mockReg )
747+ require .NotNil (t , upserted .AuthConfig , "default auth config must survive reconciliation" )
748+ assert .Equal (t , authtypes .StrategyTypeHeaderInjection , upserted .AuthConfig .Type )
749+ }
750+
751+ // TestReconcile_OutgoingAuthDiscoveredWins verifies that in discovered mode the
752+ // auth resolved from the backend's own resource references takes precedence over
753+ // a conflicting config-level backends entry — the same precedence startup
754+ // discovery applies.
755+ func TestReconcile_OutgoingAuthDiscoveredWins (t * testing.T ) {
756+ t .Parallel ()
757+
758+ discovered := & authtypes.BackendAuthStrategy {Type : authtypes .StrategyTypeUpstreamInject }
759+ k8sClient , mockDisc , mockReg := newOutgoingAuthTestFixture (t , discovered )
760+
761+ reconciler := newTestReconciler (k8sClient , "default" , "test-group" , mockReg , mockDisc )
762+ reconciler .OutgoingAuth = & config.OutgoingAuthConfig {
763+ Source : "discovered" ,
764+ Backends : map [string ]* authtypes.BackendAuthStrategy {
765+ "test-server" : {Type : authtypes .StrategyTypeHeaderInjection },
766+ },
767+ }
768+
769+ upserted := reconcileTestServer (t , reconciler , mockReg )
770+ require .NotNil (t , upserted .AuthConfig )
771+ assert .Equal (t , authtypes .StrategyTypeUpstreamInject , upserted .AuthConfig .Type ,
772+ "discovered auth must win over the config-level entry in discovered mode" )
773+ }
774+
775+ // TestReconcile_OutgoingAuthNilConfig verifies that a nil OutgoingAuth leaves
776+ // the reconciled backend untouched (the pre-existing behavior).
777+ func TestReconcile_OutgoingAuthNilConfig (t * testing.T ) {
778+ t .Parallel ()
779+
780+ k8sClient , mockDisc , mockReg := newOutgoingAuthTestFixture (t , nil )
781+
782+ reconciler := newTestReconciler (k8sClient , "default" , "test-group" , mockReg , mockDisc )
783+
784+ upserted := reconcileTestServer (t , reconciler , mockReg )
785+ assert .Nil (t , upserted .AuthConfig , "backend without discovered auth stays unauthenticated when no config is supplied" )
786+ }
0 commit comments