Compare commits

...
Author SHA1 Message Date
Emil 44247bd94e Give every wizard test its own port and disable keep-alive pooling
coordinator / test (push) Canceled after 0s
python / test (push) Canceled after 0s
release / binaries (amd64, darwin) (push) Canceled after 0s
release / binaries (amd64, linux) (push) Canceled after 0s
release / binaries (amd64, windows) (push) Canceled after 0s
release / binaries (arm64, darwin) (push) Canceled after 0s
release / binaries (arm64, linux) (push) Canceled after 0s
release / binaries (arm64, windows) (push) Canceled after 0s
release / image (push) Canceled after 0s
users / test (push) Canceled after 0s
release / release (push) Canceled after 0s
2026-08-03 01:43:48 +03:00
Emil 7c1d0dc568 Tell non-admins why the console is off-limits instead of bouncing them to the dashboard
coordinator / test (push) Canceled after 0s
python / test (push) Canceled after 0s
release / binaries (amd64, darwin) (push) Canceled after 0s
release / binaries (amd64, linux) (push) Canceled after 0s
release / binaries (amd64, windows) (push) Canceled after 0s
release / binaries (arm64, darwin) (push) Canceled after 0s
release / binaries (arm64, linux) (push) Canceled after 0s
release / binaries (arm64, windows) (push) Canceled after 0s
release / image (push) Canceled after 0s
users / test (push) Canceled after 0s
release / release (push) Canceled after 0s
2026-08-03 01:34:22 +03:00
5 changed files with 32 additions and 10 deletions
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os" "os"
@@ -25,7 +26,10 @@ func newTestServer(t *testing.T, sup Supervisor) (*Server, string) {
t.Helper() t.Helper()
dir := t.TempDir() dir := t.TempDir()
server := New(testLogger(), Options{ server := New(testLogger(), Options{
Port: 0, // ephemeral: tests must never collide on the default 12700 // A distinct random port per test: Port 0 means "the default 12700" in
// the server, which would let the shared http.Client pool reuse a stale
// keep-alive connection across tests (EOF after a Shutdown).
Port: freePort(t),
ConfigPath: filepath.Join(dir, "config.json"), ConfigPath: filepath.Join(dir, "config.json"),
Dir: dir, Dir: dir,
Supervisor: sup, Supervisor: sup,
@@ -81,7 +85,9 @@ func postJSON(t *testing.T, base, path string, body any) (*httptest.ResponseReco
t.Fatal(err) t.Fatal(err)
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
client := http.Client{} // No keep-alive pooling: a pooled connection to a shut-down test server
// would surface as an EOF instead of a fresh dial.
client := http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -94,6 +100,20 @@ func postJSON(t *testing.T, base, path string, body any) (*httptest.ResponseReco
return rec, data return rec, data
} }
// freePort reserves an ephemeral port and returns it. The listener is closed
// immediately; the tiny reuse window is acceptable for tests and each test
// gets a different port, so nothing can collide or share pooled connections.
func freePort(t *testing.T) int {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
port := listener.Addr().(*net.TCPAddr).Port
_ = listener.Close()
return port
}
func mustJSON(t *testing.T, v any) string { func mustJSON(t *testing.T, v any) string {
t.Helper() t.Helper()
raw, err := json.Marshal(v) raw, err := json.Marshal(v)
@@ -26,12 +26,14 @@ var adminUserActions = map[string]bool{
} }
// requireAdmin gates a route on the session caller being an admin. It runs // requireAdmin gates a route on the session caller being an admin. It runs
// inside withUISession, which has already stamped the requester. A non-admin is // inside withUISession, which has already stamped the requester. A signed-in
// sent back to the dashboard rather than shown the panel. // non-admin is told why (and bounced to the login with the message); an
// unauthenticated caller never gets here — the gate has already sent them to
// the login page with the intended destination.
func requireAdmin(next http.Handler) http.Handler { func requireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if req, ok := authctx.From(r.Context()); !ok || !req.IsAdmin() { if req, ok := authctx.From(r.Context()); !ok || !req.IsAdmin() {
http.Redirect(w, r, "/ui", http.StatusSeeOther) http.Redirect(w, r, "/ui/login?error=admin+role+required", http.StatusSeeOther)
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
@@ -30,15 +30,15 @@ func TestRequireAdminAllowsAdminOnly(t *testing.T) {
t.Error("admin must reach the handler") t.Error("admin must reach the handler")
} }
// Plain user is redirected to the dashboard. // Plain user is redirected to the login with the reason.
reached = false reached = false
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
h.ServeHTTP(rec, adminReq(t, "user")) h.ServeHTTP(rec, adminReq(t, "user"))
if reached { if reached {
t.Error("non-admin must not reach the handler") t.Error("non-admin must not reach the handler")
} }
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/ui" { if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/ui/login?error=admin+role+required" {
t.Errorf("non-admin got %d -> %q, want 303 -> /ui", rec.Code, rec.Header().Get("Location")) t.Errorf("non-admin got %d -> %q, want 303 -> login with the admin-required error", rec.Code, rec.Header().Get("Location"))
} }
} }
+1 -1
View File
@@ -65,7 +65,7 @@ Write-Host "SciMesh $Component installed: $Target"
Write-Host "" Write-Host ""
if ($Component -eq "coordinator") { if ($Component -eq "coordinator") {
if ($AutoStart -eq "1") { if ($AutoStart -eq "1") {
Write-Host "Starting the platform and opening the control room in your browser..." Write-Host "Starting the platform and opening the admin console in your browser..."
Write-Host "(stop it with Ctrl-C; it keeps your data in ~\.scimesh)" Write-Host "(stop it with Ctrl-C; it keeps your data in ~\.scimesh)"
Write-Host "" Write-Host ""
& $Target serve --open & $Target serve --open
+1 -1
View File
@@ -78,7 +78,7 @@ fi
if [ "$COMPONENT" = "coordinator" ]; then if [ "$COMPONENT" = "coordinator" ]; then
if [ "$AUTO_START" = "1" ]; then if [ "$AUTO_START" = "1" ]; then
echo echo
echo "Starting the platform and opening the control room in your browser..." echo "Starting the platform and opening the admin console in your browser..."
echo "(stop it with Ctrl-C; it keeps your data in ~/.scimesh)" echo "(stop it with Ctrl-C; it keeps your data in ~/.scimesh)"
echo echo
exec "$TARGET" serve --open exec "$TARGET" serve --open