Files
felhom.eu/hub/internal/wgsync/reconciler_test.go
T
admin fbeeacb124 hub: S1 wgsync (pinned-SSH push + declarative reconciler) + /admin/wg API + env wiring
internal/wgsync: x/crypto/ssh client with ssh.FixedHostKey pin (no insecure
fallback), forced-command exec, ok/applied response contract; Reconciler pushes
the FULL peer list on Trigger or 5-min tick (drift repair by construction).
internal/api/wg.go: PUT/GET /admin/wg/endpoint + POST/DELETE/GET /admin/wg/peers,
global-key-only, pubkey in body (base64 vs URL), sync ok|deferred|disabled.
main.go: WG_ENDPOINT_SSH_* env wiring, disabled-with-INFO when unconfigured.
Groups B/C/D tests incl. in-process SSH server; red-proofs b/c/d run + reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 23:40:22 +02:00

207 lines
5.3 KiB
Go

package wgsync
// Group D — reconciler (Scenario D). Non-hollow: asserts the exact pushed payloads (full list,
// deterministic order, absent-after-remove), retry-after-failure, and the no-mutation drift-
// repair tick.
import (
"context"
"encoding/json"
"io"
"log"
"path/filepath"
"sync"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
type fakeSyncer struct {
mu sync.Mutex
payloads [][]byte
err error
notify chan struct{}
}
func newFakeSyncer() *fakeSyncer { return &fakeSyncer{notify: make(chan struct{}, 16)} }
func (f *fakeSyncer) Push(ctx context.Context, payload []byte) error {
f.mu.Lock()
cp := append([]byte(nil), payload...)
f.payloads = append(f.payloads, cp)
err := f.err
f.mu.Unlock()
select {
case f.notify <- struct{}{}:
default:
}
return err
}
func (f *fakeSyncer) count() int {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.payloads)
}
func (f *fakeSyncer) last() []byte {
f.mu.Lock()
defer f.mu.Unlock()
if len(f.payloads) == 0 {
return nil
}
return f.payloads[len(f.payloads)-1]
}
func waitPush(t *testing.T, f *fakeSyncer) {
t.Helper()
select {
case <-f.notify:
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for a push")
}
}
func newWGStore(t *testing.T) *store.Store {
t.Helper()
s, err := store.New(filepath.Join(t.TempDir(), "test.db"), log.New(io.Discard, "", 0))
if err != nil {
t.Fatalf("store.New: %v", err)
}
t.Cleanup(func() { s.Close() })
if err := s.SetWGEndpoint(&store.WGEndpoint{
DNSName: "ep0.example", WGPort: 443, ServerPubkey: "SPK",
TunnelSubnet: "10.77.0.0/24", PBSTunnelIP: "10.77.0.1",
}); err != nil {
t.Fatalf("SetWGEndpoint: %v", err)
}
return s
}
func TestReconciler_TriggerPushesFullList(t *testing.T) {
st := newWGStore(t)
st.AddWGPeer("PKA", "", "")
st.AddWGPeer("PKB", "", "")
fake := newFakeSyncer()
r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
r.interval = time.Hour // tick out of the picture
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go r.Run(ctx)
r.Trigger()
waitPush(t, fake)
if fake.count() != 1 {
t.Fatalf("push count = %d, want exactly 1", fake.count())
}
var doc struct {
Version int `json:"version"`
Interface string `json:"interface"`
Peers []struct {
Pubkey string `json:"pubkey"`
AllowedIP string `json:"allowed_ip"`
} `json:"peers"`
}
if err := json.Unmarshal(fake.last(), &doc); err != nil {
t.Fatalf("payload not JSON: %v", err)
}
if doc.Version != 1 || doc.Interface != "wg0" || len(doc.Peers) != 2 {
t.Fatalf("payload = %s", fake.last())
}
if doc.Peers[0].Pubkey != "PKA" || doc.Peers[0].AllowedIP != "10.77.0.2/32" ||
doc.Peers[1].Pubkey != "PKB" || doc.Peers[1].AllowedIP != "10.77.0.3/32" {
t.Errorf("peers = %+v (order/content)", doc.Peers)
}
}
func TestReconciler_FailureRetriedOnTick(t *testing.T) {
st := newWGStore(t)
st.AddWGPeer("PKA", "", "")
fake := newFakeSyncer()
fake.err = context.DeadlineExceeded // every push fails
r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
r.interval = 30 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go r.Run(ctx)
r.Trigger()
waitPush(t, fake) // the failing triggered push
waitPush(t, fake) // the tick retry — Run must not have exited on the error
if fake.count() < 2 {
t.Fatalf("push count = %d, want >=2 (retry after failure)", fake.count())
}
}
func TestReconciler_TickPushesWithoutMutations(t *testing.T) {
st := newWGStore(t) // zero peers, zero mutations
fake := newFakeSyncer()
r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
r.interval = 30 * time.Millisecond
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go r.Run(ctx)
waitPush(t, fake) // pure tick — this IS the drift repair
var doc struct {
Peers []interface{} `json:"peers"`
}
if err := json.Unmarshal(fake.last(), &doc); err != nil {
t.Fatalf("payload not JSON: %v", err)
}
if len(doc.Peers) != 0 {
t.Errorf("empty registry must push an empty (not absent) peer list: %s", fake.last())
}
}
func TestReconciler_RemovedPeerAbsentFromNextPayload(t *testing.T) {
st := newWGStore(t)
st.AddWGPeer("PKA", "", "")
st.AddWGPeer("PKB", "", "")
fake := newFakeSyncer()
r := NewReconciler(st, fake, log.New(io.Discard, "", 0))
if err := r.SyncNow(context.Background()); err != nil {
t.Fatalf("SyncNow: %v", err)
}
if err := st.RemoveWGPeer("PKA"); err != nil {
t.Fatalf("RemoveWGPeer: %v", err)
}
if err := r.SyncNow(context.Background()); err != nil {
t.Fatalf("SyncNow 2: %v", err)
}
last := string(fake.last())
// Assert the NEGATIVE: the removed pubkey is gone from the pushed bytes.
if json.Valid([]byte(last)) == false {
t.Fatalf("payload not JSON: %s", last)
}
if contains := jsonContainsPubkey(t, []byte(last), "PKA"); contains {
t.Errorf("removed peer PKA still in payload: %s", last)
}
if contains := jsonContainsPubkey(t, []byte(last), "PKB"); !contains {
t.Errorf("surviving peer PKB missing from payload: %s", last)
}
}
func jsonContainsPubkey(t *testing.T, payload []byte, pk string) bool {
t.Helper()
var doc struct {
Peers []struct {
Pubkey string `json:"pubkey"`
} `json:"peers"`
}
if err := json.Unmarshal(payload, &doc); err != nil {
t.Fatalf("payload parse: %v", err)
}
for _, p := range doc.Peers {
if p.Pubkey == pk {
return true
}
}
return false
}