// AI generated tests and not yet reviewed. package proxyprovider import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "slices" "strings" "testing" v1alpha1 "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/apis/proxyprovider/v1alpha1" operatorfake "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/clientset/versioned/fake" operatorinformers "gitea.t000-n.de/t.behrendt/authentik-kubernetes-operator/pkg/generated/informers/externalversions" authentikapi "goauthentik.io/api/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/tools/cache" ) const testOutpostID = "550e8400-e29b-41d4-a716-446655440000" func TestController_syncHandler_create(t *testing.T) { const wantPK = 42 var outpostPartialUpdateCalled bool server := newAuthentikTestServer(t, authentikTestHandlers{ proxyCreate: func(w http.ResponseWriter, _ *http.Request) { writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK}) }, outpostRetrieve: outpostRetrieveHandler(t, nil), outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) { outpostPartialUpdateCalled = true var body struct { Providers []int32 `json:"providers"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("decode outpost patch body: %v", err) } if !slices.Contains(body.Providers, wantPK) { t.Fatalf("patched providers = %v, want to contain %d", body.Providers, wantPK) } writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers}) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, testProxyProvider(), server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-pp"}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } if !outpostPartialUpdateCalled { t.Fatal("expected Authentik outpost partial update call") } got := getProxyProvider(t, ctrl, "default", "test-pp") if got.Status.PK != "42" { t.Fatalf("status.pk = %q, want 42", got.Status.PK) } } func TestController_syncHandler_create_providerAlreadyInOutpost(t *testing.T) { const wantPK = 42 var outpostPartialUpdateCalled bool server := newAuthentikTestServer(t, authentikTestHandlers{ proxyCreate: func(w http.ResponseWriter, _ *http.Request) { writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK}) }, outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}), outpostPartialUpdate: func(w http.ResponseWriter, _ *http.Request) { outpostPartialUpdateCalled = true }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, testProxyProvider(), server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-pp"}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } if outpostPartialUpdateCalled { t.Fatal("did not expect Authentik outpost partial update when provider is already present") } got := getProxyProvider(t, ctrl, "default", "test-pp") if got.Status.PK != "42" { t.Fatalf("status.pk = %q, want 42", got.Status.PK) } } func TestController_syncHandler_ensureFinalizers(t *testing.T) { pp := testProxyProvider() pp.Status.PK = "42" server := newAuthentikTestServer(t, authentikTestHandlers{}) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if !slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { t.Fatalf("finalizers = %v, want %q", got.Finalizers, DeleteAuthentikProxyProviderFinalizer) } } func TestController_syncHandler_update(t *testing.T) { pp := testProxyProvider() pp.Status.PK = "42" pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} server := newAuthentikTestServer(t, authentikTestHandlers{ allRetrieve: func(w http.ResponseWriter, _ *http.Request) { writeJSON(t, w, http.StatusOK, map[string]any{"pk": 42}) }, proxyPartialUpdate: func(w http.ResponseWriter, _ *http.Request) { writeJSON(t, w, http.StatusOK, map[string]any{"pk": 42}) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if got.Status.PK != "42" { t.Fatalf("status.pk = %q, want 42", got.Status.PK) } } func TestController_syncHandler_update_providerNotFound(t *testing.T) { pp := testProxyProvider() pp.Status.PK = "42" pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} server := newAuthentikTestServer(t, authentikTestHandlers{ allRetrieve: func(w http.ResponseWriter, _ *http.Request) { http.NotFound(w, nil) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if got.Status.PK != "" { t.Fatalf("status.pk = %q, want empty after provider not found", got.Status.PK) } } func TestController_syncHandler_delete(t *testing.T) { const wantPK int32 = 42 now := metav1.Now() pp := testProxyProvider() pp.Status.PK = "42" pp.DeletionTimestamp = &now pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} var outpostPartialUpdateCalled, destroyCalled bool server := newAuthentikTestServer(t, authentikTestHandlers{ outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}), outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) { outpostPartialUpdateCalled = true var body struct { Providers []int32 `json:"providers"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("decode outpost patch body: %v", err) } if slices.Contains(body.Providers, wantPK) { t.Fatalf("patched providers = %v, want provider %d removed", body.Providers, wantPK) } writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers}) }, proxyDestroy: func(w http.ResponseWriter, r *http.Request) { destroyCalled = true if r.Method != http.MethodDelete { t.Errorf("destroy method = %s, want DELETE", r.Method) } w.WriteHeader(http.StatusNoContent) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } if !outpostPartialUpdateCalled { t.Fatal("expected Authentik outpost partial update call") } if !destroyCalled { t.Fatal("expected Authentik destroy call") } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers) } } func TestController_syncHandler_delete_providerNotInOutpost(t *testing.T) { now := metav1.Now() pp := testProxyProvider() pp.Status.PK = "42" pp.DeletionTimestamp = &now pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} var outpostPartialUpdateCalled, destroyCalled bool server := newAuthentikTestServer(t, authentikTestHandlers{ outpostRetrieve: outpostRetrieveHandler(t, nil), outpostPartialUpdate: func(w http.ResponseWriter, _ *http.Request) { outpostPartialUpdateCalled = true }, proxyDestroy: func(w http.ResponseWriter, _ *http.Request) { destroyCalled = true w.WriteHeader(http.StatusNoContent) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } if outpostPartialUpdateCalled { t.Fatal("did not expect Authentik outpost partial update when provider is not in outpost") } if !destroyCalled { t.Fatal("expected Authentik destroy call") } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers) } } func TestController_syncHandler_delete_providerAlreadyGone(t *testing.T) { const wantPK int32 = 42 now := metav1.Now() pp := testProxyProvider() pp.Status.PK = "42" pp.DeletionTimestamp = &now pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} server := newAuthentikTestServer(t, authentikTestHandlers{ outpostRetrieve: outpostRetrieveHandler(t, []int32{wantPK}), outpostPartialUpdate: func(w http.ResponseWriter, r *http.Request) { var body struct { Providers []int32 `json:"providers"` } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Fatalf("decode outpost patch body: %v", err) } writeJSON(t, w, http.StatusOK, map[string]any{"pk": testOutpostID, "providers": body.Providers}) }, proxyDestroy: func(w http.ResponseWriter, _ *http.Request) { http.NotFound(w, nil) }, }) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err != nil { t.Fatalf("syncHandler() error = %v", err) } got := getProxyProvider(t, ctrl, pp.Namespace, pp.Name) if slices.Contains(got.Finalizers, DeleteAuthentikProxyProviderFinalizer) { t.Fatalf("finalizers = %v, want finalizer removed after 404", got.Finalizers) } } func TestController_syncHandler_notFound(t *testing.T) { server := newAuthentikTestServer(t, authentikTestHandlers{}) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, nil, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "missing"}) if err != nil { t.Fatalf("syncHandler() error = %v, want nil for missing object", err) } } func TestController_syncHandler_invalidPK(t *testing.T) { pp := testProxyProvider() pp.Status.PK = "not-a-number" pp.Finalizers = []string{DeleteAuthentikProxyProviderFinalizer} server := newAuthentikTestServer(t, authentikTestHandlers{}) t.Cleanup(server.Close) ctrl, ctx, cancel := newTestController(t, pp, server.URL) t.Cleanup(cancel) err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: pp.Namespace, Name: pp.Name}) if err == nil { t.Fatal("syncHandler() error = nil, want parse error") } if !strings.Contains(err.Error(), "error parsing PK") { t.Fatalf("syncHandler() error = %v, want PK parse error", err) } } // --- test helpers --- func testProxyProvider() *v1alpha1.ProxyProvider { return &v1alpha1.ProxyProvider{ TypeMeta: metav1.TypeMeta{ APIVersion: v1alpha1.SchemeGroupVersion.String(), Kind: "ProxyProvider", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-pp", Namespace: "default", }, Spec: v1alpha1.ProxyProviderSpec{ Name: "my-app", AuthorizationFlow: "flow-auth", InvalidationFlow: "flow-invalidate", ExternalHost: "https://app.example.com", Outpost: testOutpostID, }, } } func newTestController(t *testing.T, pp *v1alpha1.ProxyProvider, authentikURL string) (*ProxyProviderController, context.Context, context.CancelFunc) { t.Helper() ctx, cancel := context.WithCancel(context.Background()) ctrl, _, stop := newTestControllerWithContext(t, ctx, pp, authentikURL) return ctrl, ctx, func() { cancel() stop() } } func newTestControllerWithContext(t *testing.T, ctx context.Context, pp *v1alpha1.ProxyProvider, authentikURL string) (*ProxyProviderController, context.Context, func()) { t.Helper() authentikClient := newAuthentikAPIClientForTest(t, authentikURL) var objects []runtime.Object if pp != nil { objects = append(objects, pp) } proxyClient := operatorfake.NewSimpleClientset(objects...) informerFactory := operatorinformers.NewSharedInformerFactory(proxyClient, 0) proxyInformer := informerFactory.Proxyprovider().V1alpha1().ProxyProviders() ctrl := NewController(ctx, fake.NewClientset(), proxyClient, authentikClient, proxyInformer) informerFactory.Start(ctx.Done()) for informerType, synced := range informerFactory.WaitForCacheSync(ctx.Done()) { if !synced { t.Fatalf("informer %v failed to sync", informerType) } } return ctrl, ctx, func() {} } func newAuthentikAPIClientForTest(t *testing.T, serverURL string) *authentikapi.APIClient { t.Helper() u, err := url.Parse(serverURL) if err != nil { t.Fatalf("parse server URL: %v", err) } cfg := authentikapi.NewConfiguration() cfg.Scheme = u.Scheme cfg.Host = u.Host return authentikapi.NewAPIClient(cfg) } type authentikTestHandlers struct { proxyCreate http.HandlerFunc proxyDestroy http.HandlerFunc proxyPartialUpdate http.HandlerFunc allRetrieve http.HandlerFunc outpostRetrieve http.HandlerFunc outpostPartialUpdate http.HandlerFunc } func outpostRetrieveHandler(t *testing.T, providers []int32) http.HandlerFunc { t.Helper() return func(w http.ResponseWriter, _ *http.Request) { writeJSON(t, w, http.StatusOK, map[string]any{ "pk": testOutpostID, "providers": providers, }) } } func newAuthentikTestServer(t *testing.T, handlers authentikTestHandlers) *httptest.Server { t.Helper() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path switch { case path == "/api/v3/providers/proxy/" && r.Method == http.MethodPost: if handlers.proxyCreate != nil { handlers.proxyCreate(w, r) return } http.NotFound(w, r) case strings.HasPrefix(path, "/api/v3/providers/proxy/") && strings.HasSuffix(path, "/"): idPath := strings.TrimPrefix(path, "/api/v3/providers/proxy/") if idPath == "" { http.NotFound(w, r) return } switch r.Method { case http.MethodDelete: if handlers.proxyDestroy != nil { handlers.proxyDestroy(w, r) return } http.NotFound(w, r) case http.MethodPatch: if handlers.proxyPartialUpdate != nil { handlers.proxyPartialUpdate(w, r) return } http.NotFound(w, r) default: http.Error(w, "unexpected method on proxy instance", http.StatusMethodNotAllowed) } case strings.HasPrefix(path, "/api/v3/providers/all/") && strings.HasSuffix(path, "/"): if r.Method == http.MethodGet && handlers.allRetrieve != nil { handlers.allRetrieve(w, r) return } http.NotFound(w, r) case strings.HasPrefix(path, "/api/v3/outposts/instances/") && strings.HasSuffix(path, "/"): idPath := strings.TrimPrefix(path, "/api/v3/outposts/instances/") idPath = strings.TrimSuffix(idPath, "/") if idPath == "" || strings.Contains(idPath, "/") { http.NotFound(w, r) return } switch r.Method { case http.MethodGet: if handlers.outpostRetrieve != nil { handlers.outpostRetrieve(w, r) return } http.NotFound(w, r) case http.MethodPatch: if handlers.outpostPartialUpdate != nil { handlers.outpostPartialUpdate(w, r) return } http.NotFound(w, r) default: http.Error(w, "unexpected method on outpost instance", http.StatusMethodNotAllowed) } default: http.NotFound(w, r) } }) return httptest.NewServer(handler) } func writeJSON(t *testing.T, w http.ResponseWriter, status int, body any) { t.Helper() w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(body); err != nil { t.Fatalf("write JSON response: %v", err) } } func getProxyProvider(t *testing.T, ctrl *ProxyProviderController, namespace, name string) *v1alpha1.ProxyProvider { t.Helper() got, err := ctrl.proxyProviderClientset.ProxyproviderV1alpha1().ProxyProviders(namespace).Get( context.Background(), name, metav1.GetOptions{}, ) if err != nil { t.Fatalf("get ProxyProvider: %v", err) } return got }