Files
galaxy-game/backend/internal/server/handlers_admin_console_operators_test.go
T
Ilia Denisov 87a272166b
Tests · Go / test (push) Successful in 1m59s
feat(admin-console): Stage 5 — operators (admin accounts)
Add the operator-management page over *admin.Service (no new business logic).

- GET/POST /_gm/operators                       list + create operator
- POST     /_gm/operators/{user}/disable|enable  toggle access
- POST     /_gm/operators/{user}/reset-password  set a new password

Console depends on an OperatorAdmin interface (satisfied by *admin.Service) so
the page renders in tests without a database. Create POST is mounted on the
collection path; per-row disable/enable/reset are guarded by the CSRF middleware
and redirect back. Passwords are never logged.

Tests: list render, create (+ username/password assertions), username-taken
conflict, disable/enable, reset (+ password assertion), missing-password 400,
bad-CSRF 403, and unavailable 503.

Docs: backend/docs/admin-console.md page inventory extended.
2026-05-31 20:31:16 +02:00

167 lines
5.2 KiB
Go

package server
import (
"context"
"net/http"
"strings"
"testing"
"galaxy/backend/internal/admin"
"galaxy/backend/internal/adminconsole"
"galaxy/backend/internal/server/middleware/basicauth"
"go.uber.org/zap"
)
type fakeOperatorAdmin struct {
list []admin.Admin
createErr error
created admin.CreateInput
createCalls int
disableCalls int
enableCalls int
resetCalls int
lastResetUser string
lastResetPass string
}
func (f *fakeOperatorAdmin) List(context.Context) ([]admin.Admin, error) { return f.list, nil }
func (f *fakeOperatorAdmin) Create(_ context.Context, in admin.CreateInput) (admin.Admin, error) {
f.createCalls++
f.created = in
if f.createErr != nil {
return admin.Admin{}, f.createErr
}
return admin.Admin{Username: in.Username}, nil
}
func (f *fakeOperatorAdmin) Disable(_ context.Context, username string) (admin.Admin, error) {
f.disableCalls++
return admin.Admin{Username: username}, nil
}
func (f *fakeOperatorAdmin) Enable(_ context.Context, username string) (admin.Admin, error) {
f.enableCalls++
return admin.Admin{Username: username}, nil
}
func (f *fakeOperatorAdmin) ResetPassword(_ context.Context, username, password string) (admin.Admin, error) {
f.resetCalls++
f.lastResetUser = username
f.lastResetPass = password
return admin.Admin{Username: username}, nil
}
func operatorsRouter(t *testing.T, operators OperatorAdmin) (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, Operators: operators}),
})
if err != nil {
t.Fatalf("NewRouter: %v", err)
}
return handler, csrf
}
func TestConsoleOperatorsList(t *testing.T) {
fake := &fakeOperatorAdmin{list: []admin.Admin{{Username: "root"}}}
router, _ := operatorsRouter(t, fake)
rec := consoleGet(t, router, "/_gm/operators")
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
for _, want := range []string{"root", "Create operator", "Reset"} {
if !strings.Contains(rec.Body.String(), want) {
t.Errorf("operators page missing %q", want)
}
}
}
func TestConsoleOperatorCreate(t *testing.T) {
fake := &fakeOperatorAdmin{}
router, csrf := operatorsRouter(t, fake)
rec := consolePost(t, router, "/_gm/operators", "_csrf="+csrf.Token("ops")+"&username=mod&password=s3cret")
if rec.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303; body=%s", rec.Code, rec.Body.String())
}
if fake.createCalls != 1 || fake.created.Username != "mod" || fake.created.Password != "s3cret" {
t.Errorf("create recorded %d username=%q", fake.createCalls, fake.created.Username)
}
}
func TestConsoleOperatorCreateConflict(t *testing.T) {
fake := &fakeOperatorAdmin{createErr: admin.ErrUsernameTaken}
router, csrf := operatorsRouter(t, fake)
rec := consolePost(t, router, "/_gm/operators", "_csrf="+csrf.Token("ops")+"&username=root&password=x")
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d, want 409", rec.Code)
}
}
func TestConsoleOperatorDisableEnable(t *testing.T) {
fake := &fakeOperatorAdmin{}
router, csrf := operatorsRouter(t, fake)
if rec := consolePost(t, router, "/_gm/operators/root/disable", "_csrf="+csrf.Token("ops")); rec.Code != http.StatusSeeOther {
t.Fatalf("disable status = %d, want 303", rec.Code)
}
if rec := consolePost(t, router, "/_gm/operators/root/enable", "_csrf="+csrf.Token("ops")); rec.Code != http.StatusSeeOther {
t.Fatalf("enable status = %d, want 303", rec.Code)
}
if fake.disableCalls != 1 || fake.enableCalls != 1 {
t.Errorf("disable=%d enable=%d, want 1/1", fake.disableCalls, fake.enableCalls)
}
}
func TestConsoleOperatorResetPassword(t *testing.T) {
fake := &fakeOperatorAdmin{}
router, csrf := operatorsRouter(t, fake)
rec := consolePost(t, router, "/_gm/operators/root/reset-password", "_csrf="+csrf.Token("ops")+"&password=newpass")
if rec.Code != http.StatusSeeOther {
t.Fatalf("status = %d, want 303", rec.Code)
}
if fake.resetCalls != 1 || fake.lastResetUser != "root" || fake.lastResetPass != "newpass" {
t.Errorf("reset recorded %d user=%q", fake.resetCalls, fake.lastResetUser)
}
}
func TestConsoleOperatorResetPasswordMissing(t *testing.T) {
fake := &fakeOperatorAdmin{}
router, csrf := operatorsRouter(t, fake)
rec := consolePost(t, router, "/_gm/operators/root/reset-password", "_csrf="+csrf.Token("ops"))
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", rec.Code)
}
if fake.resetCalls != 0 {
t.Error("reset must not run without a password")
}
}
func TestConsoleOperatorRejectsBadCSRF(t *testing.T) {
fake := &fakeOperatorAdmin{}
router, _ := operatorsRouter(t, fake)
rec := consolePost(t, router, "/_gm/operators/root/disable", "")
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403", rec.Code)
}
if fake.disableCalls != 0 {
t.Error("disable must not run without a CSRF token")
}
}
func TestConsoleOperatorsUnavailable(t *testing.T) {
router, _ := operatorsRouter(t, nil)
rec := consoleGet(t, router, "/_gm/operators")
if rec.Code != http.StatusServiceUnavailable {
t.Fatalf("status = %d, want 503", rec.Code)
}
}