|
| 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, ®istry.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, ®istry.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