package admin // Regression: a failed room create (POST /api/rooms with link_from) must // not leave the source room with a dangling exit pointing at a room that // was never written, and must not push an undo entry for work that never // committed. The destination is committed before the source exit so the // failure happens before any source-side mutation. import ( "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "thehouseoficarus/internal/config" "thehouseoficarus/internal/world" ) func TestFailedRoomCreateRollsBackSourceExit(t *testing.T) { s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Origin\n"}) s.cfg = config.Default() s.cfg.StartingRoom = 1 // A directory at the destination path makes the destination file write // fail deterministically while the source room is still readable. if err := os.Mkdir(filepath.Join(s.dataDir, "rooms", "2.yaml"), 0o755); err != nil { t.Fatal(err) } rec := httptest.NewRecorder() s.handleRooms(rec, httptest.NewRequest(http.MethodPost, "/api/rooms", strings.NewReader(`{"name":"Next","link_from":1,"link_dir":"north"}`))) if !strings.Contains(rec.Body.String(), "error") { t.Fatalf("expected the create to fail; got %s", rec.Body.String()) } src, err := s.world.LoadRoom(1) if err != nil { t.Fatal(err) } if exit, ok := src.Exits[world.North]; ok { t.Fatalf("failed create left a dangling exit to nonexistent room %d", exit.Room) } if info := s.undoStack.Info(); info.StackSize != 0 { t.Fatalf("failed create pushed an undo entry: %+v", info) } } // The reciprocal exit still lands in the written destination file when the // create succeeds. func TestSuccessfulRoomCreateWritesReciprocalExit(t *testing.T) { s := newTestAdminServer(t, map[int]string{1: "id: 1\nname: Origin\n"}) s.cfg = config.Default() s.cfg.StartingRoom = 1 rec := httptest.NewRecorder() s.handleRooms(rec, httptest.NewRequest(http.MethodPost, "/api/rooms", strings.NewReader(`{"name":"Next","link_from":1,"link_dir":"north"}`))) if strings.Contains(rec.Body.String(), "error") { t.Fatalf("create failed: %s", rec.Body.String()) } dest, err := s.world.LoadRoom(2) if err != nil { t.Fatalf("destination room not loadable: %v", err) } if dest.Exits[world.South].Room != 1 { t.Fatalf("destination room missing its reciprocal exit south: %+v", dest.Exits) } src, err := s.world.LoadRoom(1) if err != nil { t.Fatal(err) } if src.Exits[world.North].Room != 2 { t.Fatalf("source room missing its north exit: %+v", src.Exits) } }