sdwan_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package sdwan
  2. import (
  3. "net"
  4. "path/filepath"
  5. "testing"
  6. "time"
  7. "github.com/vishvananda/netlink"
  8. )
  9. func TestSDWANManagerBasics(t *testing.T) {
  10. tmpDir := t.TempDir()
  11. storePath := filepath.Join(tmpDir, "sdwan.json")
  12. mgr := NewManager(storePath)
  13. defer mgr.Stop()
  14. if mgr == nil {
  15. t.Fatal("expected non-nil manager")
  16. }
  17. // 1. Create SD-WAN group
  18. group := &SDWANGroup{
  19. Name: "sdwan0",
  20. Mode: ModeWeighted,
  21. ProbeInterval: 3,
  22. AutoNAT: true,
  23. Comment: "Primary Internet SD-WAN",
  24. }
  25. if err := mgr.Save(group); err != nil {
  26. t.Fatalf("failed to save group: %v", err)
  27. }
  28. // Verify group is retrievable by Name and ID
  29. got, ok := mgr.Get("sdwan0")
  30. if !ok || got == nil {
  31. t.Fatal("expected to find sdwan0 by name")
  32. }
  33. if got.ID == "" {
  34. t.Fatal("expected auto-generated group ID")
  35. }
  36. // 2. Add members
  37. mem1 := &SDWANMember{
  38. Interface: "eth0",
  39. Gateway: "192.168.1.1",
  40. Weight: 70,
  41. TargetProbe: "1.1.1.1",
  42. }
  43. if err := mgr.AddMember("sdwan0", mem1); err != nil {
  44. t.Fatalf("failed adding member 1: %v", err)
  45. }
  46. mem2 := &SDWANMember{
  47. Interface: "eth1",
  48. Gateway: "192.168.2.1",
  49. Weight: 30,
  50. TargetProbe: "8.8.8.8",
  51. }
  52. if err := mgr.AddMember("sdwan0", mem2); err != nil {
  53. t.Fatalf("failed adding member 2: %v", err)
  54. }
  55. // Test duplicate member rejection
  56. if err := mgr.AddMember("sdwan0", &SDWANMember{Interface: "eth0"}); err == nil {
  57. t.Fatal("expected error on duplicate member interface")
  58. }
  59. // 3. Test Interface Resolution for Firewall
  60. resolved := mgr.ResolveGroupInterfaces("sdwan0")
  61. if len(resolved) != 2 || resolved[0] != "eth0" || resolved[1] != "eth1" {
  62. t.Fatalf("expected [eth0, eth1], got %v", resolved)
  63. }
  64. allGroups := mgr.ResolveAllSDWANGroups()
  65. if len(allGroups["sdwan0"]) != 2 {
  66. t.Fatalf("expected sdwan0 in ResolveAllSDWANGroups, got %v", allGroups)
  67. }
  68. // 4. Update Member
  69. mem1.Weight = 80
  70. if err := mgr.UpdateMember("sdwan0", mem1); err != nil {
  71. t.Fatalf("failed updating member: %v", err)
  72. }
  73. updated, _ := mgr.Get("sdwan0")
  74. if updated.Members[0].Weight != 80 {
  75. t.Fatalf("expected weight 80, got %d", updated.Members[0].Weight)
  76. }
  77. // 5. Telemetry Summary
  78. summary := mgr.GetSummary()
  79. if summary.TotalGroups != 1 || summary.ActiveGroups != 1 || summary.TotalMembers != 2 {
  80. t.Fatalf("unexpected summary: %+v", summary)
  81. }
  82. // 6. Delete Member
  83. if err := mgr.DeleteMember("sdwan0", "eth1"); err != nil {
  84. t.Fatalf("failed deleting member: %v", err)
  85. }
  86. resolvedAfterDelete := mgr.ResolveGroupInterfaces("sdwan0")
  87. if len(resolvedAfterDelete) != 1 || resolvedAfterDelete[0] != "eth0" {
  88. t.Fatalf("expected [eth0], got %v", resolvedAfterDelete)
  89. }
  90. // 7. Delete Group
  91. if err := mgr.Delete("sdwan0"); err != nil {
  92. t.Fatalf("failed deleting group: %v", err)
  93. }
  94. if len(mgr.List()) != 0 {
  95. t.Fatal("expected 0 groups after delete")
  96. }
  97. }
  98. func TestHealthMetricsCalculation(t *testing.T) {
  99. monitor := NewHealthMonitor(nil)
  100. mem := &SDWANMember{
  101. Interface: "eth0",
  102. MaxLatency: 100,
  103. MaxLoss: 10,
  104. }
  105. samples := []probeSample{
  106. {latency: 20.0, success: true, t: time.Now()},
  107. {latency: 25.0, success: true, t: time.Now()},
  108. {latency: 30.0, success: true, t: time.Now()},
  109. {latency: 0.0, success: false, t: time.Now()},
  110. }
  111. monitor.calculateMetrics(mem, samples)
  112. // 1 failure out of 4 = 25% loss
  113. if mem.PacketLoss != 25.0 {
  114. t.Errorf("expected 25.0%% loss, got %.1f", mem.PacketLoss)
  115. }
  116. // Average of 20, 25, 30 is 25.0 ms
  117. if mem.LiveLatency != 25.0 {
  118. t.Errorf("expected 25.0ms latency, got %.1f", mem.LiveLatency)
  119. }
  120. if mem.Jitter <= 0 {
  121. t.Errorf("expected positive jitter, got %.1f", mem.Jitter)
  122. }
  123. }
  124. func TestHealthHysteresisAndThresholds(t *testing.T) {
  125. mem := &SDWANMember{
  126. Interface: "eth0",
  127. MaxLatency: 250,
  128. MaxLoss: 20,
  129. Status: StatusHealthy,
  130. }
  131. // 1-4 fails: should NOT transition to StatusDown
  132. for i := 1; i <= 4; i++ {
  133. mem.ConsecutiveFails = i
  134. mem.ConsecutiveSuccesses = 0
  135. if mem.ConsecutiveFails >= 5 {
  136. mem.Status = StatusDown
  137. }
  138. if mem.Status == StatusDown {
  139. t.Fatalf("member became DOWN prematurely at %d fails", i)
  140. }
  141. }
  142. // 5th fail: transitions to StatusDown
  143. mem.ConsecutiveFails = 5
  144. if mem.ConsecutiveFails >= 5 {
  145. mem.Status = StatusDown
  146. }
  147. if mem.Status != StatusDown {
  148. t.Fatalf("expected StatusDown after 5 fails, got %s", mem.Status)
  149. }
  150. // 1st recovery success: hysteresis must keep it StatusDown
  151. prevStatus := mem.Status
  152. mem.ConsecutiveSuccesses = 1
  153. mem.ConsecutiveFails = 0
  154. if prevStatus == StatusDown && mem.ConsecutiveSuccesses < 2 {
  155. mem.Status = StatusDown
  156. }
  157. if mem.Status != StatusDown {
  158. t.Fatalf("expected StatusDown during 1st success cycle (hysteresis), got %s", mem.Status)
  159. }
  160. // 2nd recovery success: now allowed to recover
  161. mem.ConsecutiveSuccesses = 2
  162. if prevStatus == StatusDown && mem.ConsecutiveSuccesses < 2 {
  163. mem.Status = StatusDown
  164. } else {
  165. mem.Status = StatusHealthy
  166. }
  167. if mem.Status != StatusHealthy {
  168. t.Fatalf("expected StatusHealthy after 2 consecutive successes, got %s", mem.Status)
  169. }
  170. }
  171. func TestSDWANVirtualDeviceLifecycle(t *testing.T) {
  172. tmpDir := t.TempDir()
  173. storePath := filepath.Join(tmpDir, "sdwan.json")
  174. mgr := NewManager(storePath)
  175. defer mgr.Stop()
  176. group := &SDWANGroup{
  177. Name: "sdwantest0",
  178. Mode: ModeFailover,
  179. AutoNAT: false,
  180. Members: []*SDWANMember{
  181. {Interface: "lo", Gateway: "127.0.0.1", Weight: 100},
  182. },
  183. }
  184. if err := mgr.Save(group); err != nil {
  185. t.Fatalf("failed saving group: %v", err)
  186. }
  187. // Verify virtual device exists in kernel
  188. link, err := netlink.LinkByName("sdwantest0")
  189. if err != nil {
  190. t.Fatalf("expected sdwantest0 virtual device to be created in kernel: %v", err)
  191. }
  192. if link.Attrs().Flags&net.FlagUp == 0 {
  193. t.Fatalf("expected sdwantest0 to be UP")
  194. }
  195. // Delete group and verify virtual device is removed
  196. if err := mgr.Delete("sdwantest0"); err != nil {
  197. t.Fatalf("failed to delete group: %v", err)
  198. }
  199. if _, err := netlink.LinkByName("sdwantest0"); err == nil {
  200. t.Fatalf("expected sdwantest0 virtual device to be removed from kernel after group delete")
  201. }
  202. }