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) } }