557629d2bf
gates / gates (push) Successful in 2m3s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
102 lines
3.9 KiB
Go
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")
|
|
}
|
|
}
|