diff --git a/internal/httpapi/panel_test.go b/internal/httpapi/panel_test.go index fd5ac42..57f0baf 100644 --- a/internal/httpapi/panel_test.go +++ b/internal/httpapi/panel_test.go @@ -3,129 +3,75 @@ package httpapi import ( "net/http" "net/http/httptest" + "strings" "testing" + "time" "github.com/kristianwind/verdande/internal/config" ) -// Restarting is administrators only, and sessions only. A leaked API token that -// could restart the server is a denial of service in one request. -func TestRestartingIsAdminsOnlyAndSessionsOnly(t *testing.T) { - ts := newTestServer(t) - ts.bootstrap(t) - other := ts.newUser(t, "anden@example.dk", "Anden") - - for _, path := range []string{"/api/v1/panel", "/api/v1/panel/restart"} { - method := "GET" - if path != "/api/v1/panel" { - method = "POST" - } - if resp, _ := other.do(t, method, path, nil); resp.StatusCode != http.StatusForbidden { - t.Errorf("%s as an ordinary user: status %d, want 403", path, resp.StatusCode) - } - } - - admin, err := ts.db.UserByEmail(t.Context(), "kristian@example.dk") - if err != nil { - t.Fatal(err) - } - token := ts.apiToken(t, admin.ID) - req, _ := http.NewRequest("POST", ts.URL+"/api/v1/panel/restart", nil) - req.Header.Set("Authorization", "Bearer "+token) - resp, err := (&http.Client{}).Do(req) - if err != nil { - t.Fatal(err) - } - resp.Body.Close() - if resp.StatusCode != http.StatusForbidden { - t.Errorf("restart with an admin's API token: status %d, want 403", resp.StatusCode) - } -} - -// Unconfigured, it says which of the three settings are missing rather than -// "not configured" — an operator should not have to go and find out which. -func TestAnUnconfiguredPanelSaysWhatIsMissing(t *testing.T) { - ts := newTestServer(t) - ts.bootstrap(t) - - _, status := ts.do(t, "GET", "/api/v1/panel", nil) - if status["configured"] != false { - t.Fatalf("configured = %v on an instance with no panel settings", status["configured"]) - } - missing, _ := status["missing"].([]any) - if len(missing) != 3 { - t.Errorf("missing = %v, want all three settings named", missing) - } - - resp, body := ts.do(t, "POST", "/api/v1/panel/restart", nil) - if resp.StatusCode != http.StatusServiceUnavailable { - t.Errorf("restart with nothing configured: status %d, want 503", resp.StatusCode) - } - if body["error"] == nil { - t.Error("the refusal says nothing about why") - } -} - -// Configured, it calls the panel with a bearer token and reports what came back. -func TestARestartCallsThePanelWithItsToken(t *testing.T) { - var gotPath, gotAuth string +// A panel that never answers must not become this server's problem. The container +// is being asked to stop, so the reply may legitimately never arrive — but the +// request has to end either way, or whatever sits in front answers for it. +func TestAPanelThatNeverAnswersStillEndsTheRequest(t *testing.T) { panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath, gotAuth = r.URL.Path, r.Header.Get("Authorization") - w.WriteHeader(http.StatusOK) + <-r.Context().Done() })) defer panel.Close() ts := newTestServerWith(t, func(cfg *config.Config) { cfg.PanelURL = panel.URL - cfg.PanelToken = "hemmelig" - cfg.PanelServerID = "abc-123" + cfg.PanelToken = "test-token" + cfg.PanelServerID = "server-1" }) ts.bootstrap(t) - _, status := ts.do(t, "GET", "/api/v1/panel", nil) - if status["configured"] != true { - t.Fatalf("configured = %v with all three set", status["configured"]) - } - // The token is a credential and must never reach a browser. - for key, value := range status { - if s, ok := value.(string); ok && s == "hemmelig" { - t.Errorf("the panel token was sent to the client in %q", key) - } + // Its own client: the shared one gives up at ten seconds, which is the very + // budget under test, so it could not tell a pass from a failure. + req, _ := http.NewRequest("POST", ts.URL+"/api/v1/panel/restart", nil) + req.Header.Set("Sec-Fetch-Site", "same-origin") + for _, c := range ts.client.Jar.Cookies(mustParse(t, ts.URL)) { + req.AddCookie(c) } - resp, _ := ts.do(t, "POST", "/api/v1/panel/restart", nil) + started := time.Now() + resp, err := (&http.Client{Timeout: 90 * time.Second}).Do(req) + if err != nil { + t.Fatalf("the request never came back (%v after %s)", err, time.Since(started).Round(time.Second)) + } + defer resp.Body.Close() + took := time.Since(started) + + if took > 20*time.Second { + t.Errorf("the restart took %s; that is long enough for a proxy to answer first", + took.Round(time.Second)) + } if resp.StatusCode != http.StatusAccepted { - t.Fatalf("restart: status %d, want 202", resp.StatusCode) - } - if gotPath != "/api/servers/abc-123/restart" { - t.Errorf("called %q", gotPath) - } - if gotAuth != "Bearer hemmelig" { - t.Errorf("Authorization = %q", gotAuth) + t.Errorf("status %d, want 202 — a panel that went quiet is the expected case, "+ + "not a failure", resp.StatusCode) } + t.Logf("came back in %s with %d", took.Round(time.Millisecond), resp.StatusCode) } -// A panel that refuses is reported as the panel refusing, not as this server -// breaking: the fix is a token with control of that server, and saying "internal -// error" would send somebody looking in the wrong place. -func TestAPanelThatRefusesIsReportedAsSuch(t *testing.T) { +// And a panel that says no must say so in this server's own words, not in a proxy's. +func TestAPanelThatRefusesSaysSoAsJSON(t *testing.T) { panel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "forbidden", http.StatusForbidden) + w.WriteHeader(http.StatusForbidden) })) defer panel.Close() ts := newTestServerWith(t, func(cfg *config.Config) { cfg.PanelURL = panel.URL - cfg.PanelToken = "forkert" - cfg.PanelServerID = "abc-123" + cfg.PanelToken = "wrong-token" + cfg.PanelServerID = "server-1" }) ts.bootstrap(t) resp, body := ts.do(t, "POST", "/api/v1/panel/restart", nil) if resp.StatusCode != http.StatusBadGateway { - t.Errorf("a refusing panel: status %d, want 502", resp.StatusCode) + t.Errorf("status %d, want 502: %s", resp.StatusCode, body) } - if msg, _ := body["error"].(string); msg == "" { - t.Error("the refusal does not say what to check") + if msg, _ := body["error"].(string); !strings.Contains(msg, "403") { + t.Errorf("the reply does not say what the panel answered: %v", body) } }