package admin import ( "bytes" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "thehouseoficarus/internal/config" ) func newColorsTestServer(t *testing.T) (*AdminServer, string) { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "config.yaml") if err := os.WriteFile(path, []byte("tick_length: 600\ndefault_colors:\n room_name: \"74 bold\"\n"), 0644); err != nil { t.Fatal(err) } loaded, err := config.Load(path) if err != nil { t.Fatal(err) } return &AdminServer{cfg: loaded, configPath: path}, path } func TestColorsGet(t *testing.T) { s, _ := newColorsTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/colors", nil) w := httptest.NewRecorder() s.getColors(w, req) if w.Code != http.StatusOK { t.Fatalf("status %d", w.Code) } var out map[string]any if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatal(err) } cats, _ := out["categories"].([]any) if len(cats) != len(colorCategories) { t.Fatalf("categories count: got %d want %d", len(cats), len(colorCategories)) } preview, _ := out["preview"].([]any) if len(preview) < 6 { t.Fatalf("preview sections too few: %d", len(preview)) } } func TestColorsSaveAndPersist(t *testing.T) { s, path := newColorsTestServer(t) before := s.cfg.DefaultColors["room_name"] body := map[string]any{"colors": map[string]string{"room_name": "FF bold"}} b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/colors", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.saveColors(w, req) if w.Code != http.StatusOK { t.Fatalf("save status %d: %s", w.Code, w.Body.String()) } if s.cfg.DefaultColors["room_name"] == before { t.Fatalf("live map not updated") } if s.cfg.DefaultColors["room_name"] != "FF bold" { t.Fatalf("live value = %q", s.cfg.DefaultColors["room_name"]) } raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if !bytes.Contains(raw, []byte("FF bold")) { t.Fatalf("config.yaml not persisted with new value; got:\n%s", string(raw)) } } func TestColorsSaveRejectsBadSpec(t *testing.T) { s, _ := newColorsTestServer(t) body := map[string]any{"colors": map[string]string{"room_name": "zzzzz"}} b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/colors", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.saveColors(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String()) } } func TestColorsSaveRejectsUnknownKey(t *testing.T) { s, _ := newColorsTestServer(t) body := map[string]any{"colors": map[string]string{"nope": "FF"}} b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/colors", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.saveColors(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("expected 400, got %d", w.Code) } } func TestColorsOffAllowed(t *testing.T) { s, _ := newColorsTestServer(t) body := map[string]any{"colors": map[string]string{"mob": "off"}} b, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/api/colors", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() s.saveColors(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200 for 'off', got %d: %s", w.Code, w.Body.String()) } } func TestColorsReset(t *testing.T) { s, _ := newColorsTestServer(t) s.cfg.DefaultColors["room_name"] = "FF" req := httptest.NewRequest(http.MethodPost, "/api/colors/reset", nil) w := httptest.NewRecorder() s.handleColorsReset(w, req) if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } if s.cfg.DefaultColors["room_name"] != "74 bold" { t.Fatalf("reset did not restore builtin; got %q", s.cfg.DefaultColors["room_name"]) } } func TestColorsPreviewExhaustive(t *testing.T) { s, _ := newColorsTestServer(t) req := httptest.NewRequest(http.MethodGet, "/api/colors", nil) w := httptest.NewRecorder() s.getColors(w, req) var out map[string]any if err := json.Unmarshal(w.Body.Bytes(), &out); err != nil { t.Fatal(err) } seen := map[string]bool{} for _, sec := range out["preview"].([]any) { for _, ln := range sec.(map[string]any)["lines"].([]any) { for _, seg := range ln.(map[string]any)["segments"].([]any) { c := seg.(map[string]any)["c"].(string) if c != "" { seen[c] = true } } } } for _, cat := range colorCategories { if cat.Name == "currency_pickup" || cat.Name == "map_blocked" || cat.Name == "sequence" || cat.Name == "science_mod" { continue } if !seen[cat.Name] { t.Errorf("preview missing category: %s", cat.Name) } } _ = io.Discard }