87a272166b
Tests · Go / test (push) Successful in 1m59s
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.
167 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|