internal/httpd/routes_test.go

fce24ae0024be6fd87d4a306c606035782577497
gitbay/internal/httpd/routes_test.go history · blame · raw

96 lines · 3088 bytes

 1package httpd
 2
 3import (
 4	"strings"
 5	"testing"
 6
 7	"gitbay.org/gitbay/internal/config"
 8	"gitbay.org/gitbay/internal/policy"
 9)
10
11// TestViewOnlyHasNoMutatingRoutes is the structural guarantee from the plan:
12// under web.mode = "view_only" the route table must contain no mutating
13// route — not hidden ones, none at all.
14func TestViewOnlyHasNoMutatingRoutes(t *testing.T) {
15	cfg := config.Default()
16	cfg.Web.Mode = "view_only"
17	s := New(cfg, nil)
18
19	for _, r := range s.Routes() {
20		if r.Mutating {
21			t.Errorf("view_only route table contains mutating route %s %s", r.Method, r.Pattern)
22		}
23		// The only non-GETs allowed are transport endpoints, which never
24		// authenticate by web session: git upload-pack (a pure read), the
25		// receive-pack static refusal, and LFS (SSH-minted tokens).
26		if r.Method != "GET" && !strings.Contains(r.Pattern, "git-upload-pack") &&
27			!strings.Contains(r.Pattern, "git-receive-pack") && !strings.Contains(r.Pattern, "/info/lfs/") {
28			t.Errorf("view_only route table contains non-GET route %s %s", r.Method, r.Pattern)
29		}
30		for _, word := range []string{"login", "logout", "register", "edit", "new", "settings"} {
31			if strings.Contains(r.Pattern, "/"+word) {
32				t.Errorf("view_only route table contains account-mode pattern %s %s", r.Method, r.Pattern)
33			}
34		}
35	}
36}
37
38// TestAPIRouteGating: the API route exists only when [api] enabled = true.
39func TestAPIRouteGating(t *testing.T) {
40	has := func(cfg config.Config) bool {
41		for _, r := range New(cfg, nil).Routes() {
42			if r.Pattern == "/api/v1/cmd" {
43				return true
44			}
45		}
46		return false
47	}
48	if has(config.Default()) {
49		t.Fatal("API route present with api disabled (the default)")
50	}
51	cfg := config.Default()
52	cfg.API.Enabled = true
53	if !has(cfg) {
54		t.Fatal("API route missing with api enabled")
55	}
56}
57
58// TestAccountsModeHasLoginRoute is the positive counterpart: switching the
59// mode on registers the session routes.
60func TestAccountsModeHasLoginRoute(t *testing.T) {
61	cfg := config.Default()
62	cfg.Web.Mode = "accounts"
63	s := New(cfg, nil)
64	found := false
65	for _, r := range s.Routes() {
66		if r.Pattern == "/login" {
67			found = true
68		}
69	}
70	if !found {
71		t.Fatal("accounts mode is missing the /login route")
72	}
73}
74
75// TestTopLevelRouteWordsAreReserved keeps the route table and the reserved
76// username list in agreement: every literal first path segment must be an
77// unclaimable username.
78func TestTopLevelRouteWordsAreReserved(t *testing.T) {
79	cfg := config.Default()
80	cfg.Web.Mode = "accounts" // superset of accounts-mode routes
81	// /register only registers when registration.mode != "closed"
82	// (config.Default() leaves it "closed"); open it so this walk actually
83	// reaches the route the production instance runs with.
84	cfg.Registration.Mode = "open"
85	s := New(cfg, nil)
86	for _, r := range s.Routes() {
87		seg := strings.TrimPrefix(r.Pattern, "/")
88		seg, _, _ = strings.Cut(seg, "/")
89		if seg == "" || strings.HasPrefix(seg, "{") {
90			continue // wildcard or root
91		}
92		if !policy.Reserved(seg) {
93			t.Errorf("top-level route word %q is not in the reserved username list", seg)
94		}
95	}
96}