7cac910de4
Add the mail, notifications, and broadcast pages over the mail, notification,
and diplomail services (no new business logic), completing the operator console.
- GET /_gm/mail deliveries (paginated) + dead-letters
- GET /_gm/mail/deliveries/{id} delivery detail + attempts
- POST /_gm/mail/deliveries/{id}/resend re-enqueue a non-sent delivery
- GET /_gm/notifications notifications + dead-letters + malformed
- GET/POST /_gm/broadcast multi-game admin diplomatic broadcast
Console depends on MailAdmin / NotificationAdmin / DiplomailAdmin interfaces
(satisfied by the concrete services); pages render in tests without a database.
Delivery detail and dead-letters live under /_gm/mail/deliveries/* and
/_gm/mail/... static segments to avoid a param/static route conflict. Resend
and broadcast flow through the CSRF guard.
Tests: mail page, delivery detail (+ not-found), resend (+ bad-CSRF),
notifications overview, broadcast form + send (input assertions) + bad game
ids, and unavailable. Plus an integration test that drives /_gm end to end
through the real gateway → backend (401 challenge + authenticated dashboard).
Docs: backend/docs/admin-console.md page inventory completed.
64 lines
2.0 KiB
Go
64 lines
2.0 KiB
Go
package integration_test
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/integration/testenv"
|
|
)
|
|
|
|
// TestAdminConsole_ThroughGateway verifies the full operator-console pipe:
|
|
// the edge gateway reverse-proxies `/_gm/*` to the backend, the backend
|
|
// enforces the admin Basic Auth and relays its 401 challenge unchanged, and an
|
|
// authenticated request renders the server-side dashboard.
|
|
func TestAdminConsole_ThroughGateway(t *testing.T) {
|
|
plat := testenv.Bootstrap(t, testenv.BootstrapOptions{})
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
base := plat.Gateway.HTTPURL
|
|
client := &http.Client{Timeout: 10 * time.Second}
|
|
|
|
// Unauthenticated: the backend's 401 + Basic challenge must reach the client
|
|
// through the gateway.
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/_gm/", nil)
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("GET /_gm/ (no auth): %v", err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("no-auth status = %d, want 401; body=%s", resp.StatusCode, body)
|
|
}
|
|
if !strings.Contains(resp.Header.Get("WWW-Authenticate"), "Basic") {
|
|
t.Fatalf("WWW-Authenticate = %q, want a Basic challenge", resp.Header.Get("WWW-Authenticate"))
|
|
}
|
|
|
|
// Authenticated with the bootstrap operator: the dashboard renders.
|
|
req, err = http.NewRequestWithContext(ctx, http.MethodGet, base+"/_gm/", nil)
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
req.SetBasicAuth(plat.Backend.AdminUser, plat.Backend.AdminPassword)
|
|
resp, err = client.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("GET /_gm/ (auth): %v", err)
|
|
}
|
|
body, _ = io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("authenticated status = %d, want 200; body=%s", resp.StatusCode, body)
|
|
}
|
|
if !strings.Contains(string(body), "Dashboard") {
|
|
t.Fatalf("dashboard body missing the heading; got: %s", body)
|
|
}
|
|
}
|