package offsiteapply import ( "bytes" "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "log" "os" "path/filepath" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // --- fakes --- // fakeRegistrar is the hub's key registrar. `calls` counts Register — the one call that writes to the // provider; it is the "did the bridge go to the hub" signal the settle-gate and retry tests count. type fakeRegistrar struct { calls int confirms []string gotPub string wrongFP bool err error confirmEr error panics bool } func (f *fakeRegistrar) Register(_ context.Context, pub string) (string, error) { if f.panics { panic("register must NOT be called") } f.calls++ f.gotPub = pub if f.err != nil { return "", f.err } if f.wrongFP { return "SHA256:somebody-else", nil } return FingerprintOf(pub) } func (f *fakeRegistrar) Confirm(_ context.Context, fp string) error { f.confirms = append(f.confirms, fp) return f.confirmEr } type fakeScanner struct { fp, line string err error } func (f *fakeScanner) Scan(_ context.Context, _ string, _ int) (string, string, error) { return f.fp, f.line, f.err } // realKeyGen returns ONE real pair (the bridge parses keys now — fingerprints are real). type realKeyGen struct{ priv, pub string } func (f *realKeyGen) Generate() (string, string, error) { return f.priv, f.pub, nil } // fakeProber models the provider: a key reaches the pinned server once the registrar installed it // (pinnedAfterRegister) or from the start (pinnedInitially). type fakeProber struct { reg *fakeRegistrar pinnedInitially bool pinnedAfterRegister bool calls int gotKH, gotPriv string } func (f *fakeProber) Probe(_ context.Context, _, _ string, _ int, kh, priv string) bool { f.calls++ f.gotKH, f.gotPriv = kh, priv return f.pinnedInitially || (f.pinnedAfterRegister && f.reg.calls > 0) } type fakeEnabler struct { err error calls int gotHost string gotKnownHost string gotPriv string gotQuotaGB int } func (f *fakeEnabler) ConfigureOffbox(_ context.Context, host, _ string, _ int, _, privPEM, knownHosts string, quotaGB int) error { f.calls++ f.gotHost, f.gotKnownHost, f.gotPriv, f.gotQuotaGB = host, knownHosts, privPEM, quotaGB return f.err } var testPriv, testPub = func() (string, string) { p, q, err := ED25519KeyGen{}.Generate() if err != nil { panic(err) } return p, q }() var otherPriv, otherPub = func() (string, string) { p, q, _ := ED25519KeyGen{}.Generate() return p, q }() func newBridge(t *testing.T, o config.OffsiteConfig) (*Bridge, *fakeRegistrar, *fakeProber, *fakeEnabler, *bytes.Buffer) { t.Helper() cfg := &config.Config{} cfg.Offsite = o reg := &fakeRegistrar{} pr := &fakeProber{reg: reg, pinnedAfterRegister: true} en := &fakeEnabler{} var logbuf bytes.Buffer b := &Bridge{ Cfg: cfg, Registrar: reg, Scanner: &fakeScanner{fp: "SHA256:goodfp", line: "[h]:23 ssh-ed25519 AAAAKEY"}, KeyGen: &realKeyGen{priv: testPriv, pub: testPub}, Prober: pr, Enabler: en, MarkerPath: filepath.Join(t.TempDir(), "offbox", "applied_marker"), Logger: log.New(&logbuf, "", 0), } return b, reg, pr, en, &logbuf } func goodOffsite() config.OffsiteConfig { return config.OffsiteConfig{Enabled: true, Type: "shared", Host: "h", User: "u", Port: 23, RepoPath: "/home/felhom-repo", QuotaGB: 50, HostFingerprint: "SHA256:goodfp"} } // A fresh box: verify host key → register the PUBLIC key → prove the pin → configure → confirm → marker. // What crosses to the hub is a public key and nothing else. func TestBridge_FreshBoxRegistersPublicKeyOnly(t *testing.T) { b, reg, pr, en, logbuf := newBridge(t, goodOffsite()) if err := b.Reconcile(context.Background()); err != nil { t.Fatalf("reconcile: %v", err) } if reg.calls != 1 || reg.gotPub != testPub { t.Fatalf("register calls=%d pub=%q", reg.calls, reg.gotPub) } if strings.Contains(reg.gotPub, "PRIVATE") { t.Fatal("the private key was sent to the hub") } if pr.calls != 2 || pr.gotKH != "[h]:23 ssh-ed25519 AAAAKEY" { t.Fatalf("probe calls=%d kh=%q (want before+after register, pinned to the scanned host key)", pr.calls, pr.gotKH) } fp, _ := FingerprintOf(testPub) if en.calls != 1 || en.gotPriv != testPriv || en.gotQuotaGB != 50 { t.Fatalf("enabler: %+v", en) } if len(reg.confirms) != 1 || reg.confirms[0] != fp { t.Fatalf("confirm = %v, want [%s]", reg.confirms, fp) } if _, err := os.Stat(b.MarkerPath); err != nil { t.Fatalf("marker not persisted: %v", err) } if !strings.Contains(logbuf.String(), "append-only") { t.Fatalf("log does not say append-only:\n%s", logbuf.String()) } } // THE MIGRATION: a box upgraded from the password era has an installed (unpinned) key and an applied // marker written under the OLD descriptor hash. The new hash ("|pinned-v1") re-applies once; the SAME // key is registered (no new keypair) and comes back pinned. func TestBridge_UpgradedBoxReRegistersSameKey(t *testing.T) { b, reg, _, en, _ := newBridge(t, goodOffsite()) b.Existing = func() string { return otherPriv } b.KeyGen = nil // must not be needed o := goodOffsite() if err := os.MkdirAll(filepath.Dir(b.MarkerPath), 0o700); err != nil { t.Fatal(err) } // The pre-v0.289.0 marker for this exact descriptor: if err := os.WriteFile(b.MarkerPath, []byte(legacyDescriptorHash(o)), 0o600); err != nil { t.Fatal(err) } if err := b.Reconcile(context.Background()); err != nil { t.Fatalf("reconcile: %v", err) } if reg.calls != 1 || reg.gotPub != otherPub { t.Fatalf("the upgraded box must re-register its EXISTING key: calls=%d pub=%q", reg.calls, reg.gotPub) } if en.gotPriv != otherPriv { t.Fatal("the existing key was not kept") } } // A restart / descriptor change on a box whose key is already pinned: no hub write at all. func TestBridge_AlreadyPinnedNeedsNoRegister(t *testing.T) { b, reg, pr, en, _ := newBridge(t, goodOffsite()) b.Existing = func() string { return testPriv } pr.pinnedInitially = true if err := b.Reconcile(context.Background()); err != nil { t.Fatalf("reconcile: %v", err) } if reg.calls != 0 || en.calls != 1 { t.Fatalf("register=%d enable=%d", reg.calls, en.calls) } } // The registrar answered but the key still does not reach the PINNED server (e.g. an unpinned line // won): refuse — never configure a key that can delete. func TestBridge_RegisteredButNotPinnedRefuses(t *testing.T) { b, reg, pr, en, _ := newBridge(t, goodOffsite()) pr.pinnedAfterRegister = false if err := b.Reconcile(context.Background()); err == nil { t.Fatal("a key that does not reach the pinned server must refuse") } if reg.calls != 1 || en.calls != 0 { t.Fatalf("register=%d enable=%d", reg.calls, en.calls) } if _, err := os.Stat(b.MarkerPath); err == nil { t.Fatal("marker persisted after a refusal") } } func TestBridge_HubInstalledADifferentKeyRefuses(t *testing.T) { b, reg, _, en, _ := newBridge(t, goodOffsite()) reg.wrongFP = true if err := b.Reconcile(context.Background()); err == nil || en.calls != 0 { t.Fatalf("err=%v enable=%d", err, en.calls) } } // Host-key mismatch → refuse before anything touches the hub. func TestBridge_HostKeyMismatchRefuses(t *testing.T) { b, reg, pr, en, _ := newBridge(t, goodOffsite()) b.Scanner = &fakeScanner{fp: "SHA256:EVIL", line: "x"} reg.panics = true if err := b.Reconcile(context.Background()); err == nil || !strings.Contains(err.Error(), "MISMATCH") { t.Fatalf("err = %v", err) } if pr.calls != 0 || en.calls != 0 { t.Fatal("probe/configure ran after a host-key mismatch") } } func TestBridge_IdempotentMarker(t *testing.T) { b, reg, _, _, _ := newBridge(t, goodOffsite()) if err := b.Reconcile(context.Background()); err != nil { t.Fatal(err) } reg.panics = true if err := b.Reconcile(context.Background()); err != nil { t.Fatalf("second reconcile: %v", err) } } func TestBridge_RegisterFailIsFailSafe(t *testing.T) { b, reg, _, en, _ := newBridge(t, goodOffsite()) reg.err = errors.New("hub down") if err := b.Reconcile(context.Background()); err == nil { t.Fatal("want error") } if en.calls != 0 { t.Fatal("configured after a failed register") } if _, err := os.Stat(b.MarkerPath); err == nil { t.Fatal("marker persisted after failure") } } // A failed confirm leaves an extra PINNED line (cannot delete) — not a failure of the apply. func TestBridge_ConfirmFailureIsNotFatal(t *testing.T) { b, reg, _, en, logbuf := newBridge(t, goodOffsite()) reg.confirmEr = errors.New("hub blip") if err := b.Reconcile(context.Background()); err != nil { t.Fatalf("reconcile: %v", err) } if en.calls != 1 || !strings.Contains(logbuf.String(), "confirm key") { t.Fatalf("enable=%d log=%s", en.calls, logbuf.String()) } } func TestBridge_DisabledNoOp(t *testing.T) { o := goodOffsite() o.Enabled = false b, reg, _, en, _ := newBridge(t, o) reg.panics = true if err := b.Reconcile(context.Background()); err != nil || en.calls != 0 { t.Fatalf("err=%v enable=%d", err, en.calls) } } // legacyDescriptorHash is the pre-v0.289.0 marker key, byte for byte as v0.288.0 computed it. func legacyDescriptorHash(o config.OffsiteConfig) string { sum := sha256.Sum256([]byte(fmt.Sprintf("%s|%s|%s|%d|%s|%s|%d", o.Type, o.Host, o.User, o.Port, o.RepoPath, o.HostFingerprint, o.QuotaGB))) return hex.EncodeToString(sum[:]) }