aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/owner_routes_test.go
blob: 84f48c8c228f4944a9bd5f75e8efd21aac8be452 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package admin

import (
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

	"thehouseoficarus/internal/config"
)

// The production router must not leave file-only mutations or undo/redo on
// the HTTP goroutine. A rejecting dispatcher prevents ALL API handlers from
// touching files, including future routes and read endpoints.
func TestAPIRoutesRequireOwnerAdmission(t *testing.T) {
	h, _ := newEditorHarness(t)
	tasks := 0
	s, err := NewServer(ServerDeps{
		Config: config.Default(), Accounts: h.g.AccountStore, World: h.g.World, Items: h.g.ItemStore,
		Objects: h.g.ObjectStore, Mobs: h.g.MobStore, DataDir: h.dir,
		ReloadCourses: h.g.CourseStore.ReloadAll, RewriteRoomIDs: h.g.MigrateRoomIDsOnOwner,
		EvictRoom: h.g.EvictCasinoRoom, RebuildCraftIndex: h.g.RebuildCraftIndex,
		ConfigPath: filepath.Join(h.dir, "config.yaml"),
		RunOnOwner: func(func()) bool { tasks++; return false },
	})
	if err != nil {
		t.Fatal(err)
	}
	cookieRec := httptest.NewRecorder()
	s.setAuthCookie(cookieRec, "alice")
	cookie := cookieRec.Result().Cookies()[0]

	routes := []string{
		"GET /api/map", "GET /api/dashboard", "GET /api/undo/state",
		"POST /api/rooms", "PUT /api/rooms/2", "DELETE /api/rooms/2",
		"POST /api/rooms/new-empty", "POST /api/rooms/link", "DELETE /api/rooms/link",
		"POST /api/rooms/insert", "POST /api/rooms/remove", "POST /api/rooms/move",
		"POST /api/rooms/rename", "POST /api/rooms/swap", "POST /api/rooms/bulk-edit",
		"POST /api/rooms/bulk-delete", "POST /api/rooms/resolve-duplicate",
		"POST /api/room-dirs/create", "POST /api/room-dirs/rename",
		"POST /api/courses", "PUT /api/courses/test", "DELETE /api/courses/test",
		"POST /api/triggers", "PUT /api/triggers/test", "DELETE /api/triggers/test",
		"PUT /api/files?path=test.yaml", "POST /api/files?path=test.yaml",
		"DELETE /api/files?path=test.yaml", "POST /api/files/rename",
		"POST /api/colors", "POST /api/colors/reset",
		"POST /api/undo/undo", "POST /api/undo/redo",
	}
	for _, entity := range []string{"objects", "items", "mobs", "drops", "hazards", "techs", "modules"} {
		for _, route := range []string{"POST /api/%s", "PUT /api/%s/test", "DELETE /api/%s/test", "POST /api/%s/rename", "POST /api/%s/move", "POST /api/%s/dirs"} {
			routes = append(routes, strings.ReplaceAll(route, "%s", entity))
		}
	}
	for _, route := range routes {
		t.Run(route, func(t *testing.T) {
			method, path, _ := strings.Cut(route, " ")
			r := httptest.NewRequest(method, path, strings.NewReader(`{}`))
			r.AddCookie(cookie)
			rec := httptest.NewRecorder()
			before := tasks
			s.ServeHTTP(rec, r)
			if tasks != before+1 || rec.Code != http.StatusServiceUnavailable {
				t.Fatalf("dispatcher calls=%d status=%d", tasks-before, rec.Code)
			}
		})
	}
	before := tasks
	rec := httptest.NewRecorder()
	s.ServeHTTP(rec, httptest.NewRequest("POST", "/api/rooms", nil))
	if tasks != before || rec.Code != http.StatusUnauthorized {
		t.Fatal("unauthenticated request reached owner")
	}
}