Files
felhom.eu/hub/internal/offsitekeys/service_test.go
T
2026-10-04 08:56:56 +02:00

296 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package offsitekeys
import (
"context"
"strings"
"log"
"os"
"path/filepath"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
func svcFixture(t *testing.T) (*Service, *fakeFS, *[]string) {
t.Helper()
st, err := store.New(filepath.Join(t.TempDir(), "hub.db"), log.New(os.Stderr, "", 0))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
cfg := `{"offsite":{"enabled":true,"type":"shared","host":"u1-sub4.example","user":"u1-sub4","port":23,"repo_path":"/home/felhom-repo","host_fingerprint":"SHA256:host"}}`
if err := st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "k", RetrievalPassword: "p", ConfigJSON: cfg}); err != nil {
t.Fatal(err)
}
if err := st.SaveOneTimeSecret("c1", "SubPw1%"); err != nil {
t.Fatal(err)
}
fs := newFS()
var events []string
s := &Service{Store: st, Reg: &Registrar{Dialer: fakeDialer{fs}},
Emit: func(_, typ, _, _, _, _ string) { events = append(events, typ) }}
return s, fs, &events
}
// Register → confirm → the window is refused while weekly windows are off; an operator one-shot opens
// it (deleting line FIRST); the box's close removes it; a drop beyond the bound alarms.
func TestWindow_GrantOpenCloseAndDropAlarm(t *testing.T) {
s, fs, events := svcFixture(t)
ctx := context.Background()
pub, fp := newKey(t)
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
t.Fatal(err)
}
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
t.Fatal(err)
}
if g, err := s.OpenWindowFor(ctx, "c1", 20); err != nil || g.Granted {
t.Fatalf("windows off: granted=%v err=%v — must refuse", g.Granted, err)
}
if err := s.Store.GrantOffsiteWindowOnce("c1"); err != nil {
t.Fatal(err)
}
g, err := s.OpenWindowFor(ctx, "c1", 20)
if err != nil || !g.Granted || g.MaxRemove != 10 {
t.Fatalf("one-shot: %+v %v", g, err)
}
if lines := ParseLines(fs.files[".ssh/authorized_keys"], "/home/felhom-repo"); !lines[0].Window || lines[0].Fingerprint != fp {
t.Fatalf("window line not first: %+v", lines)
}
if !s.Store.OffsiteWindowOpen("c1") {
t.Fatal("the ledger does not show the window open — the daily check would alarm on it")
}
if g2, _ := s.OpenWindowFor(ctx, "c1", 20); g2.Granted {
t.Fatal("the one-shot grant was not consumed")
}
// The box reports a fall of 12 (allowed 10) → offsite_window_drop.
if err := s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g.WindowID, CountAfter: 8, Outcome: "pruned"}); err != nil {
t.Fatal(err)
}
if a := audit(fs.files[".ssh/authorized_keys"], "/home/felhom-repo", false); len(a.Findings) != 0 {
t.Fatalf("window line left behind: %+v", a)
}
found := false
for _, e := range *events {
if e == EventWindowDrop {
found = true
}
}
if !found {
t.Fatalf("no %s event for a fall beyond the bound: %v", EventWindowDrop, *events)
}
}
// A window for a box that never confirmed its key is refused (nothing to scope the window to).
func TestWindow_NoConfirmedKeyRefused(t *testing.T) {
s, _, _ := svcFixture(t)
_ = s.Store.GrantOffsiteWindowOnce("c1")
pub, _ := newKey(t)
if _, err := s.RegisterKey(context.Background(), "c1", pub); err != nil {
t.Fatal(err)
}
if g, _ := s.OpenWindowFor(context.Background(), "c1", 10); g.Granted {
t.Fatal("granted without a confirmed key")
}
}
// A window the box never closes is closed by the hub at its bound: the deleting line goes, the ledger
// row closes with reason "timeout", and the operator hears offsite_window_failed.
func TestWindow_LeftOpenIsClosedByTheSweep(t *testing.T) {
s, fs, events := svcFixture(t)
ctx := context.Background()
pub, fp := newKey(t)
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
t.Fatal(err)
}
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
t.Fatal(err)
}
_ = s.Store.GrantOffsiteWindowOnce("c1")
g, err := s.OpenWindowFor(ctx, "c1", 10)
if err != nil || !g.Granted {
t.Fatalf("%+v %v", g, err)
}
// Make it overdue: the box crashed and never reported.
if err := s.Store.ForceOffsiteWindowDueForTest(g.WindowID); err != nil {
t.Fatal(err)
}
s.SweepExpiredWindows(ctx)
if a := audit(fs.files[".ssh/authorized_keys"], "/home/felhom-repo", false); len(a.Findings) != 0 {
t.Fatalf("the sweep left the deleting line: %+v", a)
}
w, _ := s.Store.GetOffsiteWindow(g.WindowID)
if w == nil || w.ClosedAt.IsZero() || w.CloseReason != "timeout" {
t.Fatalf("ledger = %+v", w)
}
last := (*events)[len(*events)-1]
if last != EventWindowFailed {
t.Fatalf("last event = %s", last)
}
}
// Decision 74 (R-823): a set-aside deletion is NOT acted on before the delay, a cancelled request deletes
// nothing, and the live repository can never be named.
func TestAbandon_DelayCancelAndScope(t *testing.T) {
s, fs, events := svcFixture(t)
ctx := context.Background()
aside := "/home/felhom-repo.orphaned-20261004"
fs.dirs[aside] = true
fs.dirs["/home/felhom-repo"] = true
for _, bad := range []string{"/home/felhom-repo", "/home/felhom-repo.orphaned-../x", "/home/other.orphaned-1"} {
if _, err := s.RequestAbandon(ctx, "c1", bad); err == nil {
t.Fatalf("accepted a non-set-aside path %q", bad)
}
}
st, err := s.RequestAbandon(ctx, "c1", aside)
if err != nil || st.State != "pending" {
t.Fatalf("%+v %v", st, err)
}
// Not due yet (7-day default): the sweep deletes nothing.
s.SweepAbandons(ctx)
if !fs.dirs[aside] {
t.Fatal("deleted BEFORE the delay")
}
// Cancelled, then made due: still nothing deleted.
if n, _ := s.CancelAbandon("c1", "operator"); n != 1 {
t.Fatalf("cancelled %d", n)
}
a, _ := s.Store.LatestOffsiteAbandon("c1", aside)
_ = s.Store.ForceOffsiteAbandonDueForTest(a.ID)
s.SweepAbandons(ctx)
if !fs.dirs[aside] {
t.Fatal("a CANCELLED request deleted the copy")
}
// A fresh request, due: deleted, and only that directory.
if _, err := s.RequestAbandon(ctx, "c1", aside); err != nil {
t.Fatal(err)
}
a, _ = s.Store.LatestOffsiteAbandon("c1", aside)
_ = s.Store.ForceOffsiteAbandonDueForTest(a.ID)
s.SweepAbandons(ctx)
if fs.dirs[aside] || !fs.dirs["/home/felhom-repo"] {
t.Fatalf("after the due sweep: %v", fs.dirs)
}
if st, _ := s.AbandonStatusFor("c1"); st.State != "deleted" {
t.Fatalf("state = %s", st.State)
}
last := (*events)[len(*events)-1]
if last != EventAbandonDeleted {
t.Fatalf("last event %s", last)
}
}
// Decision 72 (R-826): the operator clean-up keeps pinned lines and drops the rest; an empty file is fine.
func TestRemoveUnpinned_KeepsOnlyPinned(t *testing.T) {
s, fs, _ := svcFixture(t)
a, _ := newKey(t)
b, _ := newKey(t)
fs.files[".ssh/authorized_keys"] = a + "\n" + b + "\n"
n, err := s.RemoveUnpinnedKeys(context.Background(), "c1")
if err != nil || n != 2 || fs.files[".ssh/authorized_keys"] != "" {
t.Fatalf("n=%d err=%v file=%q", n, err, fs.files[".ssh/authorized_keys"])
}
if n, _ := s.RemoveUnpinnedKeys(context.Background(), "c1"); n != 0 {
t.Fatal("second run changed something")
}
}
// R-827: the daily check is read-only — no .ssh is created on a sub-account that has none.
func TestAudit_DoesNotCreateSSHDir(t *testing.T) {
fs := newFS()
delete(fs.dirs, ".ssh")
r := &Registrar{Dialer: fakeDialer{fs}}
if _, err := r.Audit(context.Background(), tgt, "pw", false); err != nil {
t.Fatal(err)
}
for _, c := range fs.cmds {
if strings.HasPrefix(c, "mkdir") {
t.Fatalf("the audit wrote: %q", c)
}
}
}
// The honest weekly removal (7 of ~17 per app, ~41 %) must fit under the cap — demo-hp's real shape:
// 9 apps × 17 = 153 snapshots, 63 removed in a week.
func TestMaxRemove_HonestWeekFits(t *testing.T) {
if MaxRemove(153) < 63 {
t.Fatalf("MaxRemove(153) = %d < 63 — every honest window would be refused", MaxRemove(153))
}
if MaxRemove(4) != 5 {
t.Fatal("floor of 5 lost")
}
}
// R-833: after a long gap the honest backlog exceeds half the snapshots, and the default cap makes the
// box's guard refuse every window. The operator's raised-cap grant opens ONE window with a larger cap;
// it is consumed, the next window has the default cap again, the close check uses the raised cap (no
// false drop alarm), and the grant is an operator event. Red-proof: drop the `if raised` assignment in
// OpenWindowFor and the first assertion fails.
func TestWindow_RaisedCapIsOneWindowOnly(t *testing.T) {
s, _, events := svcFixture(t)
ctx := context.Background()
pub, fp := newKey(t)
if _, err := s.RegisterKey(ctx, "c1", pub); err != nil {
t.Fatal(err)
}
if _, err := s.ConfirmKey(ctx, "c1", fp); err != nil {
t.Fatal(err)
}
// Without the raised grant: a plain one-shot gives half of 40 = 20.
_ = s.Store.GrantOffsiteWindowOnce("c1")
g0, err := s.OpenWindowFor(ctx, "c1", 40)
if err != nil || !g0.Granted || g0.MaxRemove != 20 {
t.Fatalf("plain grant: %+v %v — want the default cap 20", g0, err)
}
_ = s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g0.WindowID, CountAfter: 40, Outcome: "guard-refused"})
if err := s.GrantLargeWindow("c1", 30); err != nil {
t.Fatal(err)
}
g, err := s.OpenWindowFor(ctx, "c1", 40)
if err != nil || !g.Granted || g.MaxRemove != 30 {
t.Fatalf("raised grant: %+v %v — want MaxRemove 30", g, err)
}
*events = nil
if err := s.CloseWindowFor(ctx, "c1", WindowResult{WindowID: g.WindowID, CountAfter: 12, Outcome: "pruned"}); err != nil {
t.Fatal(err)
}
for _, e := range *events {
if e == EventWindowDrop {
t.Fatal("a drop of 28 under a raised cap of 30 alarmed — the close check ignored the window's own cap")
}
}
// The grant was consumed: no window without a new grant (weekly windows are off here) …
if g2, _ := s.OpenWindowFor(ctx, "c1", 12); g2.Granted {
t.Fatal("the raised grant was not consumed")
}
// … and the next granted window is back on the default cap.
_ = s.Store.GrantOffsiteWindowOnce("c1")
g3, _ := s.OpenWindowFor(ctx, "c1", 40)
if !g3.Granted || g3.MaxRemove != 20 {
t.Fatalf("next window: %+v — want the default cap 20 again", g3)
}
}
// The grant is an operator event, bounded, and only for a known customer.
func TestGrantLargeWindow_EventAndBounds(t *testing.T) {
s, _, events := svcFixture(t)
for _, bad := range []int{0, -1, MaxRemoveGrantCeiling + 1} {
if err := s.GrantLargeWindow("c1", bad); err == nil {
t.Fatalf("max_remove %d accepted", bad)
}
}
if err := s.GrantLargeWindow("nobody", 10); err == nil {
t.Fatal("a grant for an unknown customer was accepted")
}
if ok, _ := s.Store.TakeOffsiteWindowGrant("c1"); ok {
t.Fatal("a refused grant left a grant behind")
}
if err := s.GrantLargeWindow("c1", 10); err != nil {
t.Fatal(err)
}
if len(*events) != 1 || (*events)[0] != EventWindowLargeGrant {
t.Fatalf("events = %v, want one %s", *events, EventWindowLargeGrant)
}
}