diff options
Diffstat (limited to 'internal/admin')
| -rw-r--r-- | internal/admin/api_rooms.go | 9 | ||||
| -rw-r--r-- | internal/admin/casino_validate.go | 33 | ||||
| -rw-r--r-- | internal/admin/casino_validate_test.go | 52 |
3 files changed, 94 insertions, 0 deletions
diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index 84c3348..2c251bb 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -45,6 +45,11 @@ func (s *AdminServer) handleRooms(w http.ResponseWriter, r *http.Request) { m = map[string]any{} } + if err := validateRoomCasinoMap(m); err != nil { + writeJSONError(w, "invalid casino configuration: "+err.Error(), http.StatusBadRequest) + return + } + var linkFrom *int var linkDir *string var linkOneWay bool @@ -272,6 +277,10 @@ func (s *AdminServer) handleRoomByID(w http.ResponseWriter, r *http.Request) { m = map[string]any{} } m["id"] = id + if err := validateRoomCasinoMap(m); err != nil { + writeJSONError(w, "invalid casino configuration: "+err.Error(), http.StatusBadRequest) + return + } newContent, writeErr := writeMapAsYAML(path, m) if writeErr != nil { diff --git a/internal/admin/casino_validate.go b/internal/admin/casino_validate.go new file mode 100644 index 0000000..5b6902c --- /dev/null +++ b/internal/admin/casino_validate.go @@ -0,0 +1,33 @@ +package admin + +import ( + "bytes" + + "gopkg.in/yaml.v3" + "thehouseoficarus/internal/casino" +) + +// Validate before any room/link writes. Only casino fields are decoded here; +// the room editor's unrelated fields retain their existing representation. +func validateRoomCasinoMap(m map[string]any) error { + fields := map[string]any{} + for _, key := range []string{"casino_tables", "casino_machines"} { + if value, ok := m[key]; ok { + fields[key] = value + } + } + data, err := yaml.Marshal(fields) + if err != nil { + return err + } + var cfg struct { + Tables []casino.TableConfig `yaml:"casino_tables"` + Machines []casino.MachineConfig `yaml:"casino_machines"` + } + decoder := yaml.NewDecoder(bytes.NewReader(data)) + decoder.KnownFields(true) + if err := decoder.Decode(&cfg); err != nil { + return err + } + return casino.ValidateRoomConfigs(cfg.Tables, cfg.Machines) +} diff --git a/internal/admin/casino_validate_test.go b/internal/admin/casino_validate_test.go new file mode 100644 index 0000000..a0f4451 --- /dev/null +++ b/internal/admin/casino_validate_test.go @@ -0,0 +1,52 @@ +package admin + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "thehouseoficarus/internal/world" +) + +func TestRoomUpdateRejectsInvalidCasinoBeforeWriting(t *testing.T) { + dir := t.TempDir() + rooms := filepath.Join(dir, "rooms") + if err := os.MkdirAll(rooms, 0755); err != nil { + t.Fatal(err) + } + path := filepath.Join(rooms, "1.yaml") + original := "name: Original\n" + if err := os.WriteFile(path, []byte(original), 0644); err != nil { + t.Fatal(err) + } + s := &AdminServer{dataDir: dir, world: world.New(dir), undoStack: NewUndoStack(dir)} + for _, cfg := range []string{ + `{"id":"slots","game":"slots","payout":-1}`, + `{"id":"slots","game":"slots","spin_tickz":2}`, + } { + r := httptest.NewRequest(http.MethodPut, "/api/rooms/1", strings.NewReader(`{"casino_machines":[`+cfg+`]}`)) + w := httptest.NewRecorder() + s.handleRoomByID(w, r) + if w.Code != http.StatusBadRequest { + t.Fatalf("status %d: %s", w.Code, w.Body.String()) + } + data, err := os.ReadFile(path) + if err != nil || string(data) != original || s.undoStack.Info().CanUndo { + t.Fatal("invalid update changed room or undo history") + } + } +} + +func TestRoomCreateValidatesBeforeAllocatingOrLinking(t *testing.T) { + // No world/config needed: rejection must precede all mutation work. + s := &AdminServer{} + r := httptest.NewRequest(http.MethodPost, "/api/rooms", strings.NewReader(`{"casino_tables":[{"id":"cards","game":"typo"}],"link_from":1,"link_dir":"n"}`)) + w := httptest.NewRecorder() + s.handleRooms(w, r) + if w.Code != http.StatusBadRequest { + t.Fatalf("status %d: %s", w.Code, w.Body.String()) + } +} |
