Skip to content

Commit 2cfb652

Browse files
committed
Expand pkg/daemon test coverage
Add unit tests covering IPC handlers, tunnel framing/nonce/peers/send/listen, handshake, broadcast, cache, config, retransmission, stream packet handling, managed engine, services, policy loading, and webhook delivery across the daemon surface.
1 parent f374a90 commit 2cfb652

53 files changed

Lines changed: 16418 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package daemon
2+
3+
import (
4+
"encoding/json"
5+
"net"
6+
"reflect"
7+
"testing"
8+
9+
"github.com/TeoSlayer/pilotprotocol/pkg/policy"
10+
"github.com/TeoSlayer/pilotprotocol/pkg/protocol"
11+
"github.com/TeoSlayer/pilotprotocol/pkg/registry"
12+
)
13+
14+
// --- SetMemberTags / GetMemberTags ---
15+
16+
func TestSetAndGetMemberTagsEmptyByDefault(t *testing.T) {
17+
d := New(Config{})
18+
if got := d.GetMemberTags(42); got != nil {
19+
t.Fatalf("GetMemberTags(42) = %v, want nil", got)
20+
}
21+
}
22+
23+
func TestSetMemberTagsRoundTrip(t *testing.T) {
24+
d := New(Config{})
25+
want := []string{"alpha", "beta", "gamma"}
26+
d.SetMemberTags(7, want)
27+
got := d.GetMemberTags(7)
28+
if !reflect.DeepEqual(got, want) {
29+
t.Fatalf("GetMemberTags(7) = %v, want %v", got, want)
30+
}
31+
}
32+
33+
func TestSetMemberTagsIsScopedPerNetwork(t *testing.T) {
34+
d := New(Config{})
35+
d.SetMemberTags(1, []string{"one"})
36+
d.SetMemberTags(2, []string{"two"})
37+
if got := d.GetMemberTags(1); !reflect.DeepEqual(got, []string{"one"}) {
38+
t.Fatalf("network 1 tags = %v, want [one]", got)
39+
}
40+
if got := d.GetMemberTags(2); !reflect.DeepEqual(got, []string{"two"}) {
41+
t.Fatalf("network 2 tags = %v, want [two]", got)
42+
}
43+
}
44+
45+
func TestSetMemberTagsOverwritesPrevious(t *testing.T) {
46+
d := New(Config{})
47+
d.SetMemberTags(3, []string{"old"})
48+
d.SetMemberTags(3, []string{"new", "newer"})
49+
if got := d.GetMemberTags(3); !reflect.DeepEqual(got, []string{"new", "newer"}) {
50+
t.Fatalf("tags = %v, want [new newer]", got)
51+
}
52+
}
53+
54+
func TestSetMemberTagsAcceptsNilSlice(t *testing.T) {
55+
d := New(Config{})
56+
d.SetMemberTags(5, []string{"something"})
57+
d.SetMemberTags(5, nil) // clear
58+
if got := d.GetMemberTags(5); got != nil {
59+
t.Fatalf("expected nil after clearing; got %v", got)
60+
}
61+
}
62+
63+
// --- GetManagedEngine / StartManagedEngine / StopManagedEngine ---
64+
65+
func TestGetManagedEngineNilWhenAbsent(t *testing.T) {
66+
d := New(Config{})
67+
if me := d.GetManagedEngine(99); me != nil {
68+
t.Fatalf("GetManagedEngine(99) = %p, want nil", me)
69+
}
70+
}
71+
72+
// newTestManagedEngine builds a ManagedEngine without a daemon wire-up and with
73+
// a seeded peer map so cycleLoop skips the regConn-dependent Bootstrap path.
74+
func newTestManagedEngine(d *Daemon, netID uint16) *ManagedEngine {
75+
me := NewManagedEngine(netID, &registry.NetworkRules{Cycle: "10s"}, d)
76+
// Seed a dummy peer so cycleLoop's `needBootstrap` branch is false.
77+
me.peers[1] = &managedPeer{NodeID: 1}
78+
return me
79+
}
80+
81+
func TestStartManagedEngineRegistersAndCanBeRetrieved(t *testing.T) {
82+
d := New(Config{})
83+
// Insert directly to avoid the goroutine started by the public API,
84+
// which would try to call into a nil regConn.
85+
me := newTestManagedEngine(d, 100)
86+
d.managedMu.Lock()
87+
d.managed[100] = me
88+
d.managedMu.Unlock()
89+
90+
got := d.GetManagedEngine(100)
91+
if got != me {
92+
t.Fatalf("GetManagedEngine(100) = %p, want %p", got, me)
93+
}
94+
if got.netID != 100 {
95+
t.Fatalf("netID = %d, want 100", got.netID)
96+
}
97+
}
98+
99+
func TestStartManagedEngineIsIdempotent(t *testing.T) {
100+
d := New(Config{})
101+
// Pre-register directly; StartManagedEngine should detect existing entry
102+
// and early-return WITHOUT constructing + starting a new engine (which
103+
// would try to start a goroutine that needs a live regConn).
104+
first := newTestManagedEngine(d, 101)
105+
d.managedMu.Lock()
106+
d.managed[101] = first
107+
d.managedMu.Unlock()
108+
109+
d.StartManagedEngine(101, &registry.NetworkRules{Cycle: "10s"})
110+
111+
second := d.GetManagedEngine(101)
112+
if first != second {
113+
t.Fatalf("StartManagedEngine should not replace existing engine (got %p, want %p)", second, first)
114+
}
115+
}
116+
117+
func TestStopManagedEngineRemovesEntry(t *testing.T) {
118+
d := New(Config{})
119+
me := newTestManagedEngine(d, 102)
120+
// We must Start() so that Stop() can drain <-done. Pre-seeded peers make
121+
// cycleLoop skip Bootstrap, and the 10s ticker won't fire before we Stop.
122+
me.Start()
123+
d.managedMu.Lock()
124+
d.managed[102] = me
125+
d.managedMu.Unlock()
126+
127+
d.StopManagedEngine(102)
128+
if got := d.GetManagedEngine(102); got != nil {
129+
t.Fatalf("engine should be removed after StopManagedEngine; got %p", got)
130+
}
131+
}
132+
133+
func TestStopManagedEngineMissingIsNoop(t *testing.T) {
134+
d := New(Config{})
135+
// No panic, no hang.
136+
d.StopManagedEngine(12345)
137+
}
138+
139+
// --- GetPolicyRunner / StartPolicyRunner / StopPolicyRunner ---
140+
141+
func TestGetPolicyRunnerNilWhenAbsent(t *testing.T) {
142+
d := New(Config{})
143+
if pr := d.GetPolicyRunner(200); pr != nil {
144+
t.Fatalf("GetPolicyRunner(200) = %p, want nil", pr)
145+
}
146+
}
147+
148+
func TestStartPolicyRunnerInvalidJSONReturnsError(t *testing.T) {
149+
d := New(Config{})
150+
err := d.StartPolicyRunner(201, json.RawMessage("{not json"))
151+
if err == nil {
152+
t.Fatalf("expected parse error for invalid JSON")
153+
}
154+
}
155+
156+
// newTestPolicyRunner builds a PolicyRunner via NewPolicyRunner + Start with a
157+
// valid compiled policy, relying on the fact that cycleLoop's bootstrap
158+
// function logs-and-continues on a nil regConn-derived failure. If bootstrap
159+
// fails because fetchMembersWithTags panics, we cannot use Start — instead we
160+
// pre-register the runner directly in the map.
161+
162+
func TestStopPolicyRunnerMissingIsNoop(t *testing.T) {
163+
d := New(Config{})
164+
d.StopPolicyRunner(0xFFFF) // should not panic
165+
}
166+
167+
func TestGetPolicyRunnerReturnsRegisteredInstance(t *testing.T) {
168+
d := New(Config{})
169+
// Build a compiled policy + runner WITHOUT calling Start (which would
170+
// spawn a goroutine that touches nil regConn).
171+
doc, err := policy.Parse(json.RawMessage(`{"version":1,"rules":[{"name":"r1","on":"connect","match":"true","actions":[{"type":"allow"}]}]}`))
172+
if err != nil {
173+
t.Fatalf("parse: %v", err)
174+
}
175+
cp, err := policy.Compile(doc)
176+
if err != nil {
177+
t.Fatalf("compile: %v", err)
178+
}
179+
pr := NewPolicyRunner(250, cp, d)
180+
d.policyMu.Lock()
181+
d.policyRunners[250] = pr
182+
d.policyMu.Unlock()
183+
184+
if got := d.GetPolicyRunner(250); got != pr {
185+
t.Fatalf("GetPolicyRunner(250) = %p, want %p", got, pr)
186+
}
187+
}
188+
189+
func TestStopPolicyRunnerRemovesEntryFromMap(t *testing.T) {
190+
d := New(Config{})
191+
doc, err := policy.Parse(json.RawMessage(`{"version":1,"rules":[{"name":"r1","on":"connect","match":"true","actions":[{"type":"allow"}]}]}`))
192+
if err != nil {
193+
t.Fatalf("parse: %v", err)
194+
}
195+
cp, err := policy.Compile(doc)
196+
if err != nil {
197+
t.Fatalf("compile: %v", err)
198+
}
199+
pr := NewPolicyRunner(251, cp, d)
200+
// Pre-close both channels so pr.Stop() (which waits on <-done) doesn't
201+
// block us — we don't need to exercise the real cycleLoop here.
202+
close(pr.stopCh)
203+
close(pr.done)
204+
d.policyMu.Lock()
205+
d.policyRunners[251] = pr
206+
d.policyMu.Unlock()
207+
208+
d.StopPolicyRunner(251)
209+
if got := d.GetPolicyRunner(251); got != nil {
210+
t.Fatalf("runner should be removed; got %p", got)
211+
}
212+
}
213+
214+
// --- Trivial accessors ---
215+
216+
func TestDaemonIdentityNilByDefault(t *testing.T) {
217+
d := New(Config{})
218+
if id := d.Identity(); id != nil {
219+
t.Fatalf("Identity() = %p, want nil", id)
220+
}
221+
}
222+
223+
func TestDaemonTaskQueueNonNil(t *testing.T) {
224+
d := New(Config{})
225+
if q := d.TaskQueue(); q == nil {
226+
t.Fatalf("TaskQueue() should be non-nil")
227+
}
228+
}
229+
230+
func TestDaemonAddTunnelPeerRegistersPeer(t *testing.T) {
231+
d := New(Config{})
232+
addr := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345}
233+
d.AddTunnelPeer(42, addr)
234+
if !d.tunnels.HasPeer(42) {
235+
t.Fatalf("AddTunnelPeer should register peer 42")
236+
}
237+
}
238+
239+
func TestDaemonTunnelAddrNilBeforeListen(t *testing.T) {
240+
d := New(Config{})
241+
if a := d.TunnelAddr(); a != nil {
242+
t.Fatalf("TunnelAddr before Listen = %v, want nil", a)
243+
}
244+
}
245+
246+
func TestDaemonTunnelAddrPopulatedAfterListen(t *testing.T) {
247+
d := New(Config{})
248+
if err := d.tunnels.Listen("127.0.0.1:0"); err != nil {
249+
t.Fatalf("Listen: %v", err)
250+
}
251+
defer d.tunnels.Close()
252+
if a := d.TunnelAddr(); a == nil {
253+
t.Fatalf("TunnelAddr after Listen should be non-nil")
254+
}
255+
}
256+
257+
func TestDaemonAddrZeroValueByDefault(t *testing.T) {
258+
d := New(Config{})
259+
a := d.Addr()
260+
zero := protocol.Addr{}
261+
if a != zero {
262+
t.Fatalf("Addr() default = %v, want zero", a)
263+
}
264+
}
265+
266+
// --- stopping() ---
267+
268+
func TestStoppingFalseBeforeStop(t *testing.T) {
269+
d := New(Config{})
270+
if d.stopping() {
271+
t.Fatalf("stopping() should be false before Stop")
272+
}
273+
}
274+
275+
func TestStoppingTrueAfterStopChClosed(t *testing.T) {
276+
d := New(Config{})
277+
// We don't want to run the full Stop() (it touches tunnels/ipc/webhook),
278+
// so just close the stop channel directly to exercise stopping().
279+
close(d.stopCh)
280+
if !d.stopping() {
281+
t.Fatalf("stopping() should be true after stopCh is closed")
282+
}
283+
}
284+
285+
// --- stopManaged / stopPolicyRunners no-op on empty maps ---
286+
287+
func TestStopManagedOnEmptyMapIsNoop(t *testing.T) {
288+
d := New(Config{})
289+
d.stopManaged() // must not panic or deadlock
290+
}
291+
292+
func TestStopPolicyRunnersOnEmptyMapIsNoop(t *testing.T) {
293+
d := New(Config{})
294+
d.stopPolicyRunners()
295+
}
296+
297+
// --- stopManaged stops pre-registered engines ---
298+
299+
func TestStopManagedStopsAllRegisteredEngines(t *testing.T) {
300+
d := New(Config{})
301+
me1 := newTestManagedEngine(d, 300)
302+
me1.Start()
303+
me2 := newTestManagedEngine(d, 301)
304+
me2.Start()
305+
d.managedMu.Lock()
306+
d.managed[300] = me1
307+
d.managed[301] = me2
308+
d.managedMu.Unlock()
309+
310+
// stopManaged does NOT delete from the map; it only stops them.
311+
d.stopManaged()
312+
313+
// Subsequent StopManagedEngine is idempotent on already-stopped engines.
314+
d.StopManagedEngine(300)
315+
d.StopManagedEngine(301)
316+
}
317+
318+
// --- stopPolicyRunners stops pre-registered runners ---
319+
320+
func TestStopPolicyRunnersStopsAllRegisteredRunners(t *testing.T) {
321+
d := New(Config{})
322+
doc, err := policy.Parse(json.RawMessage(`{"version":1,"rules":[{"name":"r1","on":"connect","match":"true","actions":[{"type":"allow"}]}]}`))
323+
if err != nil {
324+
t.Fatalf("parse: %v", err)
325+
}
326+
cp, err := policy.Compile(doc)
327+
if err != nil {
328+
t.Fatalf("compile: %v", err)
329+
}
330+
pr1 := NewPolicyRunner(400, cp, d)
331+
close(pr1.stopCh)
332+
close(pr1.done)
333+
pr2 := NewPolicyRunner(401, cp, d)
334+
close(pr2.stopCh)
335+
close(pr2.done)
336+
d.policyMu.Lock()
337+
d.policyRunners[400] = pr1
338+
d.policyRunners[401] = pr2
339+
d.policyMu.Unlock()
340+
341+
d.stopPolicyRunners()
342+
// Map is not cleared, but runners are stopped; explicit StopPolicyRunner is idempotent.
343+
d.StopPolicyRunner(400)
344+
d.StopPolicyRunner(401)
345+
}

0 commit comments

Comments
 (0)