Files
felhom.eu/hub/internal/web/r544_r599_host_delete_test.go
T

102 lines
3.9 KiB
Go

package web
import (
"log"
"net/http"
"net/url"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// R-544: an acknowledged host delete with an escrow logs the DEMOTION, never "escrow deleted: true" —
// the line an operator would read as a household's last key being destroyed.
func TestHostDelete_LogSaysEscrowDemotedNotDeleted(t *testing.T) {
s, st := newTestServer(t)
var logBuf strings.Builder
s.logger = log.New(&logBuf, "", 0)
if err := st.UpsertHost(&store.Host{HostID: "esc-host", CustomerID: "c2", APIKey: "k"}); err != nil {
t.Fatal(err)
}
if _, _, err := st.SaveHostEscrow("esc-host", []byte("blob"), "fp", "p", "2026-07-01T00:00:00Z", ""); err != nil {
t.Fatal(err)
}
rr := postHostDelete(t, s, "esc-host", url.Values{"confirm_host_id": {"esc-host"}, "delete_escrow": {"1"}})
if rr.Code != http.StatusSeeOther {
t.Fatalf("acked delete = %d, want 303: %s", rr.Code, rr.Body.String())
}
out := logBuf.String()
if strings.Contains(out, "escrow deleted") {
t.Errorf("log still says the escrow was deleted:\n%s", out)
}
if !strings.Contains(out, "escrow custody demoted to retained") {
t.Errorf("log must state the demotion:\n%s", out)
}
// And it is true: the blob survives as retained custody (a delete would be the bug the log implied).
if n, err := st.CountSupersededEscrow("esc-host"); err != nil || n == 0 {
t.Errorf("retained escrow count = 0 after a host delete — custody was destroyed, not demoted")
}
}
// A host with no escrow says so, not "demoted".
func TestHostDelete_LogNoEscrow(t *testing.T) {
if got := hostDeleteEscrowEffect(false, nil); got != "no key escrow held" {
t.Fatalf("no-escrow effect = %q", got)
}
}
// R-599: an ONLINE refusal names how long ago the last report arrived and when deletion opens,
// computed from the CONFIGURED stale threshold (45m here — not the 30m code default).
func TestHostDelete_OnlineRefusalSaysWhenItOpens(t *testing.T) {
s, st := newTestServer(t)
s.staleThreshold = 45 * time.Minute
if err := st.UpsertHost(&store.Host{HostID: "gone-vm", CustomerID: "c1", APIKey: "k"}); err != nil {
t.Fatal(err)
}
if err := st.SaveHostReport("gone-vm", "c1", []byte(`{}`), store.HostReportDenorm{}); err != nil {
t.Fatal(err)
}
h, err := st.GetHost("gone-vm")
if err != nil || h == nil || h.LastReportAt == nil {
t.Fatalf("host/last report: %v", err)
}
opens := h.LastReportAt.Add(45 * time.Minute).UTC().Format("15:04")
rr := postHostDelete(t, s, "gone-vm", url.Values{"confirm_host_id": {"gone-vm"}, "delete_escrow": {"1"}})
if rr.Code != http.StatusConflict {
t.Fatalf("online delete = %d, want 409", rr.Code)
}
body := rr.Body.String()
for _, want := range []string{"min ago", "deletion opens at " + opens + " UTC", "45m0s"} {
if !strings.Contains(body, want) {
t.Errorf("host-delete 409 body missing %q:\n%s", want, body)
}
}
// The customer delete cascade's ONLINE refusal says the same.
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", CustomerName: "c1", APIKey: "a", RetrievalPassword: "p"}); err != nil {
t.Fatal(err)
}
rr = postDelete(t, s, "c1", cascadeForm("c1", 1))
if rr.Code != http.StatusConflict || !strings.Contains(rr.Body.String(), "deletion opens at "+opens+" UTC") {
t.Errorf("cascade ONLINE refusal = %d %q, want 409 naming the opening time", rr.Code, rr.Body.String())
}
}
// The arithmetic, with a fixed clock: last report 10 minutes ago at 17:12 UTC, threshold 45m.
func TestDeletionOpensAt_Arithmetic(t *testing.T) {
s := &Server{staleThreshold: 45 * time.Minute}
last := time.Date(2026, 9, 20, 17, 12, 0, 0, time.UTC)
got := s.deletionOpensAt(&last, last.Add(10*time.Minute))
for _, want := range []string{"10 min ago (17:12 UTC)", "deletion opens at 17:57 UTC", "(45m0s)"} {
if !strings.Contains(got, want) {
t.Errorf("deletionOpensAt = %q, missing %q", got, want)
}
}
if s.deletionOpensAt(nil, last) != "" {
t.Error("no last report → no timing sentence")
}
}