package server import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "housecarl/internal/api" "housecarl/internal/jarl" "housecarl/internal/jarlsets" "housecarl/internal/model" "housecarl/internal/store" ) // TestJarlSets: a set from Jarl, through Housecarl's Jarl client against a // fake Jarl, filled after a change, with its status and warnings. func TestJarlSets(t *testing.T) { records := []map[string]string{ {"name": "nas.example.com.", "type": "A", "data": "10.0.10.20"}, {"name": "nas.example.com.", "type": "AAAA", "data": "2001:db8:10::20"}, {"name": "files.example.com.", "type": "CNAME", "data": "nas.example.com."}, {"name": "far.example.com.", "type": "A", "data": "192.0.2.9"}, } fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer jarl-token" { w.WriteHeader(401) return } switch r.URL.Path { case "/api/v1/zones": json.NewEncoder(w).Encode([]map[string]any{{"id": 1, "name": "example.com.", "view": "internal", "type": "primary"}}) case "/api/v1/zones/1/records": if r.URL.Query().Get("disabled") != "0" { t.Errorf("records asked without disabled=0: %s", r.URL) } json.NewEncoder(w).Encode(records) default: w.WriteHeader(404) } })) t.Cleanup(fake.Close) jc, err := jarl.New(fake.URL, "jarl-token", "", false) if err != nil { t.Fatal(err) } js := &jarlsets.Resolver{Source: jc} e := newEnvWith(t, Config{JarlSets: js}) js.State, js.Connected = e.eng.State, e.srv.Connected js.Apply = func(ctx context.Context) error { return e.eng.ApplyCurrent(ctx) } js.Audit = func(ctx context.Context, set, detail string) { e.eng.Audit(ctx, nil, "set.jarl", set, "success", detail) } ctx := context.Background() e.call(e.token, "POST", "/zones", model.Zone{Name: "lan", Interfaces: []string{"eth1"}}, nil) // 10.0.10.0/24 set := model.AddressSet{Name: "nas", Jarl: &model.JarlSource{Names: []model.JarlName{{Name: "files.example.com"}, {Name: "far.example.com", View: "internal"}}, Within: []string{"lan"}}} if code := e.call(e.token, "POST", "/sets", set, nil); code != 201 { t.Fatalf("create: %d", code) } if code := e.call(e.token, "POST", "/sets", model.AddressSet{Name: "bad", Entries: []string{"10.0.0.1"}, Jarl: set.Jarl}, nil); code != 422 { t.Errorf("entries in a Jarl set: %d", code) } var st api.JarlSet if code := e.call(e.token, "POST", "/sets/nas/jarl/refresh", nil, &st); code != 200 { t.Fatalf("refresh: %d", code) } if strings.Join(st.Entries, ",") != "10.0.10.20" || len(st.Names) != 2 || !st.Configured || st.Read == nil { t.Fatalf("status %+v", st) } files := st.Names[0] if files.Found != "internal" || strings.Join(files.Chain, ",") != "nas.example.com" || len(files.Addresses) != 2 || !files.Addresses[0].Used || files.Addresses[1].Used || !strings.Contains(files.Addresses[1].Why, "not within lan") { t.Errorf("files: %+v", files) } var status api.Status e.call(e.token, "GET", "/status", nil, &status) n := 0 for _, p := range status.Problems { if p.Code == "jarl_outside" { n++ } } if n != 2 { t.Errorf("jarl_outside warnings: %d (%+v)", n, status.Problems) } // A Jarl set can be renamed (its addresses go by its names in Jarl). if code := e.call(e.token, "PATCH", "/sets/nas", map[string]any{"name": "storage"}, nil); code != 200 { t.Errorf("rename: %d", code) } if code := e.call(e.token, "GET", "/sets/trusted/jarl", nil, nil); code != 404 { t.Errorf("no such set: %d", code) } e.call(e.token, "POST", "/sets", model.AddressSet{Name: "trusted", Entries: []string{"10.0.10.0/24"}}, nil) if code := e.call(e.token, "GET", "/sets/trusted/jarl", nil, nil); code != 400 { t.Errorf("a set not from Jarl: %d", code) } evs, _ := e.srv.store.ListAudit(ctx, store.AuditFilter{Action: "set.jarl"}) if len(evs) != 1 || evs[0].Detail != "added 10.0.10.20" { t.Errorf("audit %+v", evs) } }