182 lines
6.1 KiB
Go
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)
|
|
}
|
|
}
|