Files
felhom.eu/hub/internal/offsitekeys/service_test.go
T

223 lines
7.6 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")
}
}