package hammerhead import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "git.veillax.com/Veillax/hammerhead/internal/secretbox" "git.veillax.com/Veillax/reef/model" ) func edgeConfigOf(t *testing.T, s *Server, token string) model.EdgeConfig { t.Helper() ts := httptest.NewServer(s.Handler()) defer ts.Close() req, _ := http.NewRequest("GET", ts.URL+"/edge/config?v=0", nil) req.Header.Set("Authorization", "Bearer "+token) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var cfg model.EdgeConfig if resp.StatusCode != 200 { t.Fatalf("edge config: %s", resp.Status) } json.NewDecoder(resp.Body).Decode(&cfg) return cfg } func tokenFile(t *testing.T, dir string) string { b, err := os.ReadFile(filepath.Join(dir, "self-edge.env")) if err != nil { t.Fatal(err) } return strings.TrimSpace(strings.SplitN(strings.SplitN(string(b), "BLACKTIP_TOKEN=", 2)[1], "\n", 2)[0]) } var key = secretbox.GenerateKey() func TestSelfEdgeServesTheDashboardWithoutAProxy(t *testing.T) { dir := t.TempDir() opts := Options{MasterKey: key, SelfDomain: "Hammerhead.Example.com", SelfTarget: "127.0.0.1:8080"} s, err := New(dir, opts) if err != nil { t.Fatal(err) } token := tokenFile(t, dir) cfg := edgeConfigOf(t, s, token) if len(cfg.Mappings) != 1 || len(cfg.Local) != 1 { t.Fatalf("mappings=%d local=%d", len(cfg.Mappings), len(cfg.Local)) } m := cfg.Mappings[0] if m.Proto != model.ProtoHTTPS || m.Hostname != "hammerhead.example.com" || m.Port != 443 || !m.RedirectHTTP || !m.Enabled || len(m.Targets) != 1 || m.Targets[0].Host != "127.0.0.1" || m.Targets[0].Port != 8080 || m.Targets[0].SiteID != cfg.Local[0] { t.Fatalf("unexpected dashboard route: %+v", m) } if m.Auth.Protected() { t.Fatal("the dashboard has its own login and must not sit behind resource auth") } edgeID := s.st.Settings.SelfEdgeID if info, _ := os.Stat(filepath.Join(dir, "self-edge.env")); info.Mode().Perm()&0o077 != 0 && os.PathSeparator == '/' { t.Fatalf("token file is readable by others: %v", info.Mode()) } s.Close() // a restart keeps the same edge and token instead of making a new one s2, err := New(dir, opts) if err != nil { t.Fatal(err) } defer s2.Close() if s2.st.Settings.SelfEdgeID != edgeID || tokenFile(t, dir) != token || len(s2.st.Edges) != 1 { t.Fatal("restart changed the self edge") } if len(edgeConfigOf(t, s2, token).Mappings) != 1 { t.Fatal("route missing after restart") } } func TestNoSelfEdgeUnlessAsked(t *testing.T) { dir := t.TempDir() s, err := New(dir, Options{MasterKey: key}) if err != nil { t.Fatal(err) } defer s.Close() if len(s.st.Edges) != 0 { t.Fatal("an edge appeared without HAMMERHEAD_SELF_DOMAIN") } if _, err := os.Stat(filepath.Join(dir, "self-edge.env")); err == nil { t.Fatal("token file written without HAMMERHEAD_SELF_DOMAIN") } } func TestSavingSettingsKeepsTheSelfEdge(t *testing.T) { dir := t.TempDir() opts := Options{AdminEmail: "admin@test.local", AdminPassword: "password123", MasterKey: key, SelfDomain: "hh.example.com", SelfTarget: "127.0.0.1:8080"} s, err := New(dir, opts) if err != nil { t.Fatal(err) } token := tokenFile(t, dir) do := apiClient(t, s) if code, b := do("PUT", "/api/admin/settings", map[string]any{"dashboardUrl": "https://hh.example.com", "acmeEmail": "me@example.com"}); code != 200 { t.Fatalf("settings: %d %s", code, b) } s.Close() s2, err := New(dir, opts) if err != nil { t.Fatal(err) } defer s2.Close() if len(s2.st.Edges) != 1 || tokenFile(t, dir) != token { t.Fatalf("saving settings lost the self edge: %d edges", len(s2.st.Edges)) } }