package keaapi import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client/fake" v1alpha1 "git.unkin.net/unkin/kea-operator/api/v1alpha1" ) const testToken = "s3cr3t" func newTestServer(t *testing.T) *httptest.Server { t.Helper() scheme := runtime.NewScheme() if err := clientgoscheme.AddToScheme(scheme); err != nil { t.Fatal(err) } if err := v1alpha1.AddToScheme(scheme); err != nil { t.Fatal(err) } cl := fake.NewClientBuilder().WithScheme(scheme).Build() srv := &Server{ Store: &K8sStore{Client: cl, Namespace: "dhcp-system"}, Token: testToken, Log: logr.Discard(), } return httptest.NewServer(srv.Handler()) } func do(t *testing.T, method, url, token string, body any) *http.Response { t.Helper() var buf bytes.Buffer if body != nil { if err := json.NewEncoder(&buf).Encode(body); err != nil { t.Fatal(err) } } req, err := http.NewRequestWithContext(context.Background(), method, url, &buf) if err != nil { t.Fatal(err) } if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } return resp } func TestSubnetCRUDLifecycle(t *testing.T) { ts := newTestServer(t) defer ts.Close() base := ts.URL + "/api/v1/subnets/net13" // PUT create resp := do(t, http.MethodPut, base, testToken, SubnetAPI{ Subnet: "198.18.13.0/24", Pools: []string{"198.18.13.200 - 198.18.13.220"}, Routers: []string{"198.18.13.1"}, NextServer: "198.18.19.19", }) if resp.StatusCode != http.StatusOK { t.Fatalf("PUT create: got %d", resp.StatusCode) } var created SubnetAPI _ = json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() if created.Name != "net13" { t.Errorf("name not stamped from URL, got %q", created.Name) } // GET resp = do(t, http.MethodGet, base, testToken, nil) if resp.StatusCode != http.StatusOK { t.Fatalf("GET: got %d", resp.StatusCode) } resp.Body.Close() // PUT update (idempotent upsert) resp = do(t, http.MethodPut, base, testToken, SubnetAPI{Subnet: "198.18.13.0/24", DomainName: "main.unkin.net"}) if resp.StatusCode != http.StatusOK { t.Fatalf("PUT update: got %d", resp.StatusCode) } var updated SubnetAPI _ = json.NewDecoder(resp.Body).Decode(&updated) resp.Body.Close() if updated.DomainName != "main.unkin.net" { t.Errorf("update not applied") } // LIST resp = do(t, http.MethodGet, ts.URL+"/api/v1/subnets", testToken, nil) var list []SubnetAPI _ = json.NewDecoder(resp.Body).Decode(&list) resp.Body.Close() if len(list) != 1 { t.Errorf("expected 1 subnet, got %d", len(list)) } // DELETE resp = do(t, http.MethodDelete, base, testToken, nil) if resp.StatusCode != http.StatusNoContent { t.Fatalf("DELETE: got %d", resp.StatusCode) } resp.Body.Close() // GET after delete -> 404 (drives provider drift handling) resp = do(t, http.MethodGet, base, testToken, nil) if resp.StatusCode != http.StatusNotFound { t.Fatalf("GET after delete: expected 404, got %d", resp.StatusCode) } resp.Body.Close() } func TestAuthRequired(t *testing.T) { ts := newTestServer(t) defer ts.Close() // no token resp := do(t, http.MethodGet, ts.URL+"/api/v1/subnets", "", nil) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401 without token, got %d", resp.StatusCode) } resp.Body.Close() // wrong token resp = do(t, http.MethodGet, ts.URL+"/api/v1/subnets", "nope", nil) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401 with bad token, got %d", resp.StatusCode) } resp.Body.Close() } func TestHealthzOpen(t *testing.T) { ts := newTestServer(t) defer ts.Close() resp := do(t, http.MethodGet, ts.URL+"/healthz", "", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("healthz should be open, got %d", resp.StatusCode) } resp.Body.Close() } func TestPutSubnetValidation(t *testing.T) { ts := newTestServer(t) defer ts.Close() resp := do(t, http.MethodPut, ts.URL+"/api/v1/subnets/bad", testToken, SubnetAPI{}) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400 for missing subnet, got %d", resp.StatusCode) } resp.Body.Close() } func TestClientClassCRUD(t *testing.T) { ts := newTestServer(t) defer ts.Close() base := ts.URL + "/api/v1/clientclasses/UEFI-64" resp := do(t, http.MethodPut, base, testToken, ClientClassAPI{ ArchHex: []string{"0x0007", "0x0009"}, BootFileName: "/ipxe.efi", }) if resp.StatusCode != http.StatusOK { t.Fatalf("PUT class: got %d", resp.StatusCode) } resp.Body.Close() resp = do(t, http.MethodPut, ts.URL+"/api/v1/clientclasses/empty", testToken, ClientClassAPI{}) if resp.StatusCode != http.StatusBadRequest { t.Fatalf("expected 400 for class with no match, got %d", resp.StatusCode) } resp.Body.Close() }