package server import ( "context" "net/http" "net/http/httptest" "strings" "testing" "galaxy/backend/internal/adminconsole" "galaxy/backend/internal/server/middleware/basicauth" "galaxy/backend/internal/user" "github.com/google/uuid" "go.uber.org/zap" ) // fakeUserAdmin records calls so the console handlers can be exercised without // a database. type fakeUserAdmin struct { page user.AccountPage account user.Account getErr error sanctionCalls int lastSanction user.ApplySanctionInput entitlementCall int lastEntitlement user.ApplyEntitlementInput softDeleteCalls int lastSoftActor user.ActorRef } func (f *fakeUserAdmin) ListAccounts(context.Context, int, int) (user.AccountPage, error) { return f.page, nil } func (f *fakeUserAdmin) GetAccount(context.Context, uuid.UUID) (user.Account, error) { return f.account, f.getErr } func (f *fakeUserAdmin) ApplySanction(_ context.Context, in user.ApplySanctionInput) (user.Account, error) { f.sanctionCalls++ f.lastSanction = in return f.account, nil } func (f *fakeUserAdmin) ApplyEntitlement(_ context.Context, in user.ApplyEntitlementInput) (user.Account, error) { f.entitlementCall++ f.lastEntitlement = in return f.account, nil } func (f *fakeUserAdmin) SoftDelete(_ context.Context, _ uuid.UUID, actor user.ActorRef) error { f.softDeleteCalls++ f.lastSoftActor = actor return nil } func newUsersConsoleRouter(t *testing.T, users UserAdmin) (http.Handler, *adminconsole.CSRF) { t.Helper() csrf := adminconsole.NewCSRF([]byte("test-key")) handler, err := NewRouter(RouterDependencies{ Logger: zap.NewNop(), AdminVerifier: basicauth.NewStaticVerifier("secret"), AdminConsole: NewAdminConsoleHandlers(AdminConsoleDeps{CSRF: csrf, Users: users}), }) if err != nil { t.Fatalf("NewRouter: %v", err) } return handler, csrf } func TestConsoleUsersList(t *testing.T) { fake := &fakeUserAdmin{page: user.AccountPage{ Items: []user.Account{ {UserID: uuid.New(), Email: "alice@example.test", UserName: "alice"}, {UserID: uuid.New(), Email: "bob@example.test", UserName: "bob", PermanentBlock: true}, }, Page: 1, PageSize: 50, Total: 2, }} router, _ := newUsersConsoleRouter(t, fake) req := httptest.NewRequest(http.MethodGet, "/_gm/users", nil) req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, want := range []string{"alice@example.test", "bob@example.test", "blocked", "page 1"} { if !strings.Contains(body, want) { t.Errorf("users list missing %q", want) } } } func TestConsoleUserDetailRendersForms(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{ UserID: id, Email: "alice@example.test", UserName: "alice", Entitlement: user.EntitlementSnapshot{Tier: user.TierFree}, }} router, csrf := newUsersConsoleRouter(t, fake) req := httptest.NewRequest(http.MethodGet, "/_gm/users/"+id.String(), nil) req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, want := range []string{ "alice@example.test", "Permanently block", "Update entitlement", "Soft-delete account", csrf.Token("ops"), } { if !strings.Contains(body, want) { t.Errorf("user detail missing %q", want) } } } func TestConsoleUserDetailNotFound(t *testing.T) { fake := &fakeUserAdmin{getErr: user.ErrAccountNotFound} router, _ := newUsersConsoleRouter(t, fake) req := httptest.NewRequest(http.MethodGet, "/_gm/users/"+uuid.New().String(), nil) req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404", rec.Code) } if !strings.Contains(rec.Body.String(), "not found") { t.Error("expected a not-found message") } } func TestConsoleUserBlock(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{UserID: id}} router, csrf := newUsersConsoleRouter(t, fake) form := "_csrf=" + csrf.Token("ops") + "&reason_code=spam" req := httptest.NewRequest(http.MethodPost, "http://galaxy.lan/_gm/users/"+id.String()+"/block", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://galaxy.lan") req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303; body=%s", rec.Code, rec.Body.String()) } if fake.sanctionCalls != 1 { t.Fatalf("ApplySanction called %d times, want 1", fake.sanctionCalls) } if fake.lastSanction.SanctionCode != user.SanctionCodePermanentBlock { t.Errorf("sanction code = %q, want permanent_block", fake.lastSanction.SanctionCode) } if fake.lastSanction.Scope != "account" { t.Errorf("scope = %q, want account", fake.lastSanction.Scope) } if fake.lastSanction.ReasonCode != "spam" { t.Errorf("reason = %q, want spam", fake.lastSanction.ReasonCode) } if fake.lastSanction.Actor.Type != "admin" || fake.lastSanction.Actor.ID != "ops" { t.Errorf("actor = %+v, want admin/ops", fake.lastSanction.Actor) } if fake.lastSanction.UserID != id { t.Errorf("sanction user id = %s, want %s", fake.lastSanction.UserID, id) } } func TestConsoleUserBlockMissingReason(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{UserID: id}} router, csrf := newUsersConsoleRouter(t, fake) form := "_csrf=" + csrf.Token("ops") req := httptest.NewRequest(http.MethodPost, "http://galaxy.lan/_gm/users/"+id.String()+"/block", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://galaxy.lan") req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400", rec.Code) } if fake.sanctionCalls != 0 { t.Errorf("ApplySanction must not be called without a reason") } } func TestConsoleUserBlockRejectsBadCSRF(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{UserID: id}} router, _ := newUsersConsoleRouter(t, fake) req := httptest.NewRequest(http.MethodPost, "http://galaxy.lan/_gm/users/"+id.String()+"/block", strings.NewReader("reason_code=spam")) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://galaxy.lan") req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusForbidden { t.Fatalf("status = %d, want 403", rec.Code) } if fake.sanctionCalls != 0 { t.Errorf("ApplySanction must not run when the CSRF token is missing") } } func TestConsoleUserEntitlement(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{UserID: id}} router, csrf := newUsersConsoleRouter(t, fake) form := "_csrf=" + csrf.Token("ops") + "&tier=monthly&source=admin&reason_code=promo" req := httptest.NewRequest(http.MethodPost, "http://galaxy.lan/_gm/users/"+id.String()+"/entitlement", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://galaxy.lan") req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303; body=%s", rec.Code, rec.Body.String()) } if fake.entitlementCall != 1 { t.Fatalf("ApplyEntitlement called %d times, want 1", fake.entitlementCall) } if fake.lastEntitlement.Tier != user.TierMonthly { t.Errorf("tier = %q, want monthly", fake.lastEntitlement.Tier) } if fake.lastEntitlement.Actor.ID != "ops" { t.Errorf("actor id = %q, want ops", fake.lastEntitlement.Actor.ID) } } func TestConsoleUserSoftDelete(t *testing.T) { id := uuid.New() fake := &fakeUserAdmin{account: user.Account{UserID: id}} router, csrf := newUsersConsoleRouter(t, fake) form := "_csrf=" + csrf.Token("ops") req := httptest.NewRequest(http.MethodPost, "http://galaxy.lan/_gm/users/"+id.String()+"/soft-delete", strings.NewReader(form)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Origin", "https://galaxy.lan") req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303", rec.Code) } if got := rec.Header().Get("Location"); got != "/_gm/users" { t.Errorf("redirect Location = %q, want /_gm/users", got) } if fake.softDeleteCalls != 1 { t.Fatalf("SoftDelete called %d times, want 1", fake.softDeleteCalls) } if fake.lastSoftActor.ID != "ops" { t.Errorf("soft-delete actor = %q, want ops", fake.lastSoftActor.ID) } } func TestConsoleUsersUnavailable(t *testing.T) { router, _ := newUsersConsoleRouter(t, nil) // no user service wired req := httptest.NewRequest(http.MethodGet, "/_gm/users", nil) req.SetBasicAuth("ops", "secret") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want 503", rec.Code) } }