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 }