tenseleyflow/shithub / d133b1c

Browse files

test: OptionalUser populates IsSuspended and skips bind on stale epoch

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
d133b1cc4607840d680b386523d4777ffab7f85a
Parents
fbcae7f
Tree
a2b41fc

1 changed file

StatusFile+-
M internal/web/middleware/middleware_test.go 66 0
internal/web/middleware/middleware_test.gomodified
@@ -12,12 +12,78 @@ import (
1212
 	"strings"
1313
 	"testing"
1414
 	"time"
15
+
16
+	"github.com/tenseleyFlow/shithub/internal/auth/session"
1517
 )
1618
 
1719
 func dropLogger() *slog.Logger {
1820
 	return slog.New(slog.NewTextHandler(io.Discard, nil))
1921
 }
2022
 
23
+// TestOptionalUser_PopulatesIsSuspended is the audit's regression
24
+// guard for finding C1: when the underlying user record carries a
25
+// non-NULL `suspended_at` (relayed by the lookup as IsSuspended=true),
26
+// the CurrentUser bound into context must reflect it. Every handler
27
+// that constructs a policy.UserActor reads `viewer.IsSuspended`
28
+// downstream — without propagation here, the suspension gate is
29
+// silently bypassed.
30
+func TestOptionalUser_PopulatesIsSuspended(t *testing.T) {
31
+	t.Parallel()
32
+
33
+	bind := func(t *testing.T, suspended bool) CurrentUser {
34
+		t.Helper()
35
+		var captured CurrentUser
36
+		next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37
+			captured = CurrentUserFromContext(r.Context())
38
+			w.WriteHeader(http.StatusOK)
39
+		})
40
+		lookup := func(ctx context.Context, id int64) (UserLookupResult, error) {
41
+			return UserLookupResult{
42
+				Username: "alice", SessionEpoch: 7, IsSuspended: suspended,
43
+			}, nil
44
+		}
45
+		// Inject the session directly into context — bypasses the
46
+		// SessionLoader middleware (no cookie store needed for this
47
+		// unit test of the lookup-to-context plumbing).
48
+		s := &session.Session{UserID: 42, Epoch: 7}
49
+		req := httptest.NewRequest(http.MethodGet, "/", nil)
50
+		req = req.WithContext(context.WithValue(req.Context(), sessionKey, s))
51
+		rec := httptest.NewRecorder()
52
+		OptionalUser(lookup)(next).ServeHTTP(rec, req)
53
+		return captured
54
+	}
55
+
56
+	if u := bind(t, true); !u.IsSuspended {
57
+		t.Errorf("suspended=true: got IsSuspended=false, want true")
58
+	}
59
+	if u := bind(t, false); u.IsSuspended {
60
+		t.Errorf("suspended=false: got IsSuspended=true, want false")
61
+	}
62
+}
63
+
64
+// TestOptionalUser_StaleEpochSkipsBind is the corollary: when the
65
+// recorded session epoch doesn't match the current users.session_epoch
66
+// (because the user logged out everywhere), the binding is skipped so
67
+// downstream RequireUser bounces them to /login. This existed before
68
+// the C1 fix; covering it here pins the contract.
69
+func TestOptionalUser_StaleEpochSkipsBind(t *testing.T) {
70
+	t.Parallel()
71
+	var captured CurrentUser
72
+	next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73
+		captured = CurrentUserFromContext(r.Context())
74
+	})
75
+	lookup := func(ctx context.Context, id int64) (UserLookupResult, error) {
76
+		return UserLookupResult{Username: "alice", SessionEpoch: 99}, nil
77
+	}
78
+	s := &session.Session{UserID: 42, Epoch: 7}
79
+	req := httptest.NewRequest(http.MethodGet, "/", nil)
80
+	req = req.WithContext(context.WithValue(req.Context(), sessionKey, s))
81
+	OptionalUser(lookup)(next).ServeHTTP(httptest.NewRecorder(), req)
82
+	if !captured.IsAnonymous() {
83
+		t.Errorf("stale epoch: got CurrentUser{ID=%d}, want anonymous", captured.ID)
84
+	}
85
+}
86
+
2187
 func TestRequestID_AssignsAndEchoes(t *testing.T) {
2288
 	t.Parallel()
2389
 	var captured string