Files
fancy-dns/internal/console/validate_test.go
T
mats 66f36cfac6
build / build-push (push) Successful in 1m11s
more stuff
2026-10-16 14:49:50 +02:00

182 lines
6.1 KiB
Go

package console
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
dynfake "k8s.io/client-go/dynamic/fake"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/config"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubeedit"
"git.k8s-git-k6n.net/mats/fancy-dns/internal/kubesource"
)
// validator builds a console whose cluster holds the given groups and whose
// resolver reports one group of its own, defined in its config file.
func validator(t *testing.T, saved ...config.Group) *Console {
t.Helper()
cl := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(),
map[schema.GroupVersionResource]string{
kubesource.ZoneGVR: "DNSZoneList", kubesource.GroupGVR: "ClientGroupList",
kubesource.ListGVR: "BlockListList", kubesource.OverrideGVR: "AddressOverrideList",
kubesource.RuleGVR: "DomainRuleList",
})
ed := kubeedit.New(cl, "dns")
if len(saved) > 0 {
if _, err := ed.SyncGroups(context.Background(), saved, nil, "test"); err != nil {
t.Fatal(err)
}
}
resolver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/clients":
groups := []map[string]any{{
"name": "from-file", "policy": "allow", "source": "",
"match": map[string]any{"cidrs": []string{"172.16.0.0/12"}},
}}
for _, g := range saved {
groups = append(groups, map[string]any{"name": g.Name, "policy": g.Policy, "source": "kubernetes", "match": g.Match})
}
json.NewEncoder(w).Encode(map[string]any{"groups": groups, "default_policy": "allow", "default_rate_limit": 50})
case "/api/upstreams":
json.NewEncoder(w).Encode(map[string]any{"servers": []map[string]any{{"addr": "1.1.1.1:53"}}})
default:
http.NotFound(w, r)
}
}))
t.Cleanup(resolver.Close)
rt, err := NewRuntime(resolver.URL, "")
if err != nil {
t.Fatal(err)
}
return &Console{opt: Options{Runtime: rt, Editor: ed}, log: slog.Default()}
}
func validate(t *testing.T, c *Console, body string) validateResponse {
t.Helper()
rec := httptest.NewRecorder()
c.handleClientsValidate(rec, httptest.NewRequest(http.MethodPost, "/api/clients/validate", strings.NewReader(body)))
if rec.Code != http.StatusOK {
t.Fatalf("status %d: %s", rec.Code, rec.Body)
}
var out validateResponse
if err := json.Unmarshal(rec.Body.Bytes(), &out); err != nil {
t.Fatalf("%v: %s", err, rec.Body)
}
return out
}
func hasField(list []string, f string) bool {
for _, s := range list {
if s == f {
return true
}
}
return false
}
func errFields(r validateResponse) []string {
var out []string
for _, e := range r.Errors {
out = append(out, e.Field)
}
return out
}
func TestValidateReportsEveryErrorAtOnce(t *testing.T) {
c := validator(t)
r := validate(t, c, `{"group":{"name":"Bad Name","policy":"sometimes","match":{"cidrs":["10.0.0.0/33"]},"rate_limit":-1}}`)
if r.OK {
t.Fatal("a group with four problems validated")
}
for _, want := range []string{"name", "policy", "match.cidrs", "rate_limit"} {
if !hasField(errFields(r), want) {
t.Errorf("no error for %q; got %+v", want, r.Errors)
}
}
}
func TestValidateAcceptsAGoodGroupAndExplainsIt(t *testing.T) {
c := validator(t)
r := validate(t, c, `{"group":{"name":"kids","policy":"allow","match":{"cidrs":["10.0.5.0/24"]},"allow_domains":["*.school.example"]}}`)
if !r.OK || len(r.Errors) != 0 {
t.Fatalf("errors: %+v", r.Errors)
}
if r.Position != 2 || r.Total != 2 {
t.Errorf("a new group goes last: position %d of %d", r.Position, r.Total)
}
if len(r.Summary) < 3 {
t.Errorf("summary = %v", r.Summary)
}
// 0 rate limit under a default of 50 is the surprise worth flagging.
found := false
for _, w := range r.Warnings {
found = found || w.Field == "rate_limit"
}
if !found {
t.Errorf("warnings = %+v, want the rate-limit exemption", r.Warnings)
}
}
func TestValidateEditingKeepsThePosition(t *testing.T) {
a := config.Group{Name: "a", Policy: "allow", Match: config.Match{CIDRs: []string{"10.0.0.0/8"}}}
b := config.Group{Name: "b", Policy: "allow", Match: config.Match{CIDRs: []string{"192.168.0.0/16"}}}
c := validator(t, a, b)
r := validate(t, c, `{"group":{"name":"a","policy":"deny","match":{"cidrs":["10.0.0.0/8"]}}}`)
if r.Position != 2 || r.Total != 3 { // from-file, a, b
t.Errorf("position %d of %d", r.Position, r.Total)
}
}
func TestValidateRefusesAFileGroupName(t *testing.T) {
c := validator(t)
r := validate(t, c, `{"group":{"name":"from-file","policy":"deny","match":{"cidrs":["10.0.0.0/8"]}}}`)
if r.OK || !hasField(errFields(r), "name") {
t.Errorf("a group the resolver's file owns validated: %+v", r)
}
}
func TestValidateFlagsAGroupBuriedUnderAWiderOne(t *testing.T) {
wide := config.Group{Name: "wide", Policy: "allow", Match: config.Match{CIDRs: []string{"10.0.0.0/8"}}}
c := validator(t, wide)
r := validate(t, c, `{"group":{"name":"narrow","policy":"deny","match":{"cidrs":["10.1.0.0/16"]}}}`)
if !r.OK || !r.Unreachable {
t.Errorf("ok=%v unreachable=%v warnings=%+v", r.OK, r.Unreachable, r.Warnings)
}
}
func TestValidateTestIP(t *testing.T) {
g := config.Group{Name: "lan", Policy: "deny", Match: config.Match{CIDRs: []string{"10.0.0.0/8"}}}
c := validator(t, g)
r := validate(t, c, `{"group":{"name":"lan","policy":"deny","match":{"cidrs":["10.0.0.0/8"]}},"test_ip":"10.1.2.3"}`)
if r.Test == nil || r.Test.Group != "lan" || r.Test.Default {
t.Errorf("test = %+v", r.Test)
}
r = validate(t, c, `{"group":{"name":"lan","policy":"deny","match":{"cidrs":["10.0.0.0/8"]}},"test_ip":"8.8.8.8"}`)
if r.Test == nil || !r.Test.Default {
t.Errorf("test = %+v", r.Test)
}
r = validate(t, c, `{"group":{"name":"lan","policy":"deny","match":{"cidrs":["10.0.0.0/8"]}},"test_ip":"nope"}`)
if r.Test == nil || r.Test.Valid {
t.Errorf("test = %+v", r.Test)
}
}
func TestValidateUnreachableResolverIsABadGateway(t *testing.T) {
c := validator(t)
c.opt.Runtime, _ = NewRuntime("http://127.0.0.1:1", "")
rec := httptest.NewRecorder()
c.handleClientsValidate(rec, httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"group":{"name":"x"}}`)))
if rec.Code != http.StatusBadGateway {
t.Errorf("status %d", rec.Code)
}
}