Files
authentik-kubernetes-operator/pkg/controllers/application/controller_test.go
t.behrendt 26bd576690
CD / Create tag (push) Successful in 11s
CD / Build and push (amd64) (push) Successful in 1m32s
CD / Create manifest (push) Successful in 7s
feat: vertical slice application -> provider -> binding (#4)
Co-authored-by: Timo Behrendt <t.behrendt@t00n.de>
Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
2026-05-25 17:14:35 +02:00

378 lines
11 KiB
Go

// AI generated tests and not yet reviewed.
package application
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/application/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"
)
func TestController_syncHandler_create(t *testing.T) {
const wantPK = "42"
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationCreate: func(w http.ResponseWriter, _ *http.Request) {
writeJSON(t, w, http.StatusCreated, map[string]any{"pk": wantPK})
},
})
t.Cleanup(server.Close)
ctrl, ctx, cancel := newTestController(t, testApplication(), server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: "default", Name: "test-app"})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
got := getApplication(t, ctrl, "default", "test-app")
if got.Status.PK != wantPK {
t.Fatalf("status.pk = %q, want %q", got.Status.PK, wantPK)
}
}
func TestController_syncHandler_ensureFinalizers(t *testing.T) {
app := testApplication()
app.Status.PK = "42"
server := newAuthentikTestServer(t, authentikTestHandlers{})
t.Cleanup(server.Close)
ctrl, ctx, cancel := newTestController(t, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
got := getApplication(t, ctrl, app.Namespace, app.Name)
if !slices.Contains(got.Finalizers, DeleteAuthentikApplicationFinalizer) {
t.Fatalf("finalizers = %v, want %q", got.Finalizers, DeleteAuthentikApplicationFinalizer)
}
}
func TestController_syncHandler_update(t *testing.T) {
app := testApplication()
app.Status.PK = "42"
app.Finalizers = []string{DeleteAuthentikApplicationFinalizer}
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationRetrieve: func(w http.ResponseWriter, _ *http.Request) {
writeJSON(t, w, http.StatusOK, map[string]any{"pk": "42"})
},
applicationPartialUpdate: 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, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
got := getApplication(t, ctrl, app.Namespace, app.Name)
if got.Status.PK != "42" {
t.Fatalf("status.pk = %q, want 42", got.Status.PK)
}
}
func TestController_syncHandler_update_applicationNotFound(t *testing.T) {
app := testApplication()
app.Status.PK = "42"
app.Finalizers = []string{DeleteAuthentikApplicationFinalizer}
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationRetrieve: func(w http.ResponseWriter, _ *http.Request) {
http.NotFound(w, nil)
},
})
t.Cleanup(server.Close)
ctrl, ctx, cancel := newTestController(t, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
got := getApplication(t, ctrl, app.Namespace, app.Name)
if got.Status.PK != "" {
t.Fatalf("status.pk = %q, want empty after application not found", got.Status.PK)
}
}
func TestController_syncHandler_delete(t *testing.T) {
now := metav1.Now()
app := testApplication()
app.Status.PK = "42"
app.DeletionTimestamp = &now
app.Finalizers = []string{DeleteAuthentikApplicationFinalizer}
var destroyCalled bool
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationDestroy: 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, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
if !destroyCalled {
t.Fatal("expected Authentik destroy call")
}
got := getApplication(t, ctrl, app.Namespace, app.Name)
if slices.Contains(got.Finalizers, DeleteAuthentikApplicationFinalizer) {
t.Fatalf("finalizers = %v, want finalizer removed", got.Finalizers)
}
}
func TestController_syncHandler_delete_providerAlreadyGone(t *testing.T) {
now := metav1.Now()
app := testApplication()
app.Status.PK = "42"
app.DeletionTimestamp = &now
app.Finalizers = []string{DeleteAuthentikApplicationFinalizer}
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationDestroy: func(w http.ResponseWriter, _ *http.Request) {
http.NotFound(w, nil)
},
})
t.Cleanup(server.Close)
ctrl, ctx, cancel := newTestController(t, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
got := getApplication(t, ctrl, app.Namespace, app.Name)
if slices.Contains(got.Finalizers, DeleteAuthentikApplicationFinalizer) {
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_delete_usesSlugNotPK(t *testing.T) {
now := metav1.Now()
app := testApplication()
app.Status.PK = "not-a-number"
app.DeletionTimestamp = &now
app.Finalizers = []string{DeleteAuthentikApplicationFinalizer}
var destroySlug string
server := newAuthentikTestServer(t, authentikTestHandlers{
applicationDestroy: func(w http.ResponseWriter, r *http.Request) {
destroySlug = strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/v3/core/applications/"), "/")
w.WriteHeader(http.StatusNoContent)
},
})
t.Cleanup(server.Close)
ctrl, ctx, cancel := newTestController(t, app, server.URL)
t.Cleanup(cancel)
err := ctrl.syncHandler(ctx, cache.ObjectName{Namespace: app.Namespace, Name: app.Name})
if err != nil {
t.Fatalf("syncHandler() error = %v", err)
}
if destroySlug != app.Spec.Slug {
t.Fatalf("destroy slug = %q, want %q (delete must use spec.slug, not status.pk)", destroySlug, app.Spec.Slug)
}
}
// --- test helpers ---
func testApplication() *v1alpha1.Application {
return &v1alpha1.Application{
TypeMeta: metav1.TypeMeta{
APIVersion: v1alpha1.SchemeGroupVersion.String(),
Kind: "Application",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "default",
},
Spec: v1alpha1.ApplicationSpec{
Name: "My Application",
Slug: "my-app",
Provider: 7,
},
}
}
func newTestController(t *testing.T, app *v1alpha1.Application, authentikURL string) (*ApplicationController, context.Context, context.CancelFunc) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
ctrl, _, stop := newTestControllerWithContext(t, ctx, app, authentikURL)
return ctrl, ctx, func() {
cancel()
stop()
}
}
func newTestControllerWithContext(t *testing.T, ctx context.Context, app *v1alpha1.Application, authentikURL string) (*ApplicationController, context.Context, func()) {
t.Helper()
authentikClient := newAuthentikAPIClientForTest(t, authentikURL)
var objects []runtime.Object
if app != nil {
objects = append(objects, app)
}
applicationClient := operatorfake.NewSimpleClientset(objects...)
informerFactory := operatorinformers.NewSharedInformerFactory(applicationClient, 0)
applicationInformer := informerFactory.Application().V1alpha1().Applications()
ctrl := NewController(ctx, fake.NewClientset(), applicationClient, authentikClient, applicationInformer)
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 {
applicationCreate http.HandlerFunc
applicationRetrieve http.HandlerFunc
applicationPartialUpdate http.HandlerFunc
applicationDestroy http.HandlerFunc
}
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/core/applications/" && r.Method == http.MethodPost:
if handlers.applicationCreate != nil {
handlers.applicationCreate(w, r)
return
}
http.NotFound(w, r)
case strings.HasPrefix(path, "/api/v3/core/applications/") && strings.HasSuffix(path, "/"):
slugPath := strings.TrimPrefix(path, "/api/v3/core/applications/")
if slugPath == "" {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
if handlers.applicationRetrieve != nil {
handlers.applicationRetrieve(w, r)
return
}
http.NotFound(w, r)
case http.MethodPatch:
if handlers.applicationPartialUpdate != nil {
handlers.applicationPartialUpdate(w, r)
return
}
http.NotFound(w, r)
case http.MethodDelete:
if handlers.applicationDestroy != nil {
handlers.applicationDestroy(w, r)
return
}
http.NotFound(w, r)
default:
http.Error(w, "unexpected method on application 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 getApplication(t *testing.T, ctrl *ApplicationController, namespace, name string) *v1alpha1.Application {
t.Helper()
got, err := ctrl.applicationClientset.ApplicationV1alpha1().Applications(namespace).Get(
context.Background(), name, metav1.GetOptions{},
)
if err != nil {
t.Fatalf("get Application: %v", err)
}
return got
}