manager_test.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package routing
  2. import (
  3. "encoding/json"
  4. "os"
  5. "path/filepath"
  6. "testing"
  7. )
  8. func TestDeduplicationOnLoad(t *testing.T) {
  9. tmpDir := t.TempDir()
  10. storePath := filepath.Join(tmpDir, "static_routes.json")
  11. // Create test JSON with duplicate routes
  12. routes := []RouteItem{
  13. {
  14. ID: "route-1",
  15. DstAddress: "10.0.0.0/8",
  16. Gateway: "10.42.36.254",
  17. Interface: "AWMN-Internal",
  18. RoutingTable: "main",
  19. Distance: 1,
  20. Comment: "Original",
  21. },
  22. {
  23. ID: "route-2",
  24. DstAddress: "10.0.0.0/8",
  25. Gateway: "10.42.36.254",
  26. Interface: "AWMN-Internal",
  27. RoutingTable: "main",
  28. Distance: 1,
  29. Comment: "Duplicate",
  30. },
  31. {
  32. ID: "route-3",
  33. DstAddress: "192.168.1.0/24",
  34. Gateway: "192.168.1.1",
  35. RoutingTable: "main",
  36. Distance: 1,
  37. },
  38. }
  39. data, err := json.Marshal(routes)
  40. if err != nil {
  41. t.Fatalf("failed to marshal routes: %v", err)
  42. }
  43. if err := os.WriteFile(storePath, data, 0600); err != nil {
  44. t.Fatalf("failed to write test file: %v", err)
  45. }
  46. mgr := &RoutingManager{
  47. staticRoutes: make([]RouteItem, 0),
  48. }
  49. // Read and deduplicate using the exact logic from load()
  50. seenIDs := make(map[string]bool)
  51. seenKeys := make(map[string]bool)
  52. var dedupedRev []RouteItem
  53. hasDups := false
  54. var loadedRoutes []RouteItem
  55. _ = json.Unmarshal(data, &loadedRoutes)
  56. for i := len(loadedRoutes) - 1; i >= 0; i-- {
  57. r := loadedRoutes[i]
  58. normDst := normalizeDst(r.DstAddress)
  59. tbl := r.RoutingTable
  60. if tbl == "" {
  61. tbl = "main"
  62. }
  63. routeKey := normDst + "|" + tbl + "|" + r.Gateway + "|" + r.Interface + "|1"
  64. if seenIDs[r.ID] || seenKeys[routeKey] {
  65. hasDups = true
  66. continue
  67. }
  68. seenIDs[r.ID] = true
  69. seenKeys[routeKey] = true
  70. dedupedRev = append(dedupedRev, r)
  71. }
  72. var deduped []RouteItem
  73. for i := len(dedupedRev) - 1; i >= 0; i-- {
  74. deduped = append(deduped, dedupedRev[i])
  75. }
  76. mgr.staticRoutes = deduped
  77. if !hasDups {
  78. t.Errorf("expected duplicates to be detected")
  79. }
  80. if len(mgr.staticRoutes) != 2 {
  81. t.Fatalf("expected 2 routes after dedup, got %d", len(mgr.staticRoutes))
  82. }
  83. // The latest entry for 10.0.0.0/8 should have been preserved
  84. if mgr.staticRoutes[0].ID != "route-2" || mgr.staticRoutes[0].Comment != "Duplicate" {
  85. t.Errorf("expected route-2 (latest) to be kept, got %+v", mgr.staticRoutes[0])
  86. }
  87. }
  88. func TestSaveRouteUpdateNoDuplicates(t *testing.T) {
  89. mgr := &RoutingManager{
  90. staticRoutes: []RouteItem{
  91. {
  92. ID: "route-100",
  93. DstAddress: "10.0.0.0/8",
  94. Gateway: "10.42.36.254",
  95. Interface: "AWMN-Internal",
  96. RoutingTable: "main",
  97. Distance: 1,
  98. Comment: "Original AWMN Route",
  99. },
  100. },
  101. }
  102. // 1. Edit using matching ID
  103. editReq1 := RouteItem{
  104. ID: "route-100",
  105. DstAddress: "10.0.0.0/8",
  106. Gateway: "10.42.36.253",
  107. Interface: "AWMN-Internal",
  108. RoutingTable: "main",
  109. Distance: 1,
  110. Comment: "Updated Gateway",
  111. }
  112. // Test findIndex logic
  113. foundIndex := -1
  114. for i, existing := range mgr.staticRoutes {
  115. if existing.ID == editReq1.ID {
  116. foundIndex = i
  117. break
  118. }
  119. }
  120. if foundIndex != 0 {
  121. t.Fatalf("expected foundIndex 0, got %d", foundIndex)
  122. }
  123. mgr.staticRoutes[foundIndex] = editReq1
  124. if len(mgr.staticRoutes) != 1 {
  125. t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
  126. }
  127. if mgr.staticRoutes[0].Gateway != "10.42.36.253" {
  128. t.Errorf("expected gateway 10.42.36.253, got %s", mgr.staticRoutes[0].Gateway)
  129. }
  130. // 2. Edit where UI sent kr-... ID (kernel route adoption/edit) but with OldDstAddress and OldGateway
  131. editReq2 := RouteItem{
  132. ID: "kr-254-10.0.0.0_8-10.42.36.253",
  133. OldDstAddress: "10.0.0.0/8",
  134. OldGateway: "10.42.36.253",
  135. OldRoutingTable: "main",
  136. OldInterface: "AWMN-Internal",
  137. DstAddress: "10.0.0.0/8",
  138. Gateway: "10.42.36.252",
  139. Interface: "AWMN-Internal",
  140. RoutingTable: "main",
  141. Distance: 1,
  142. CheckGateway: "internet",
  143. CheckTarget: "10.22.11.4",
  144. }
  145. foundIndex = -1
  146. // Match by OldDstAddress / OldGateway
  147. normOldDst := normalizeDst(editReq2.OldDstAddress)
  148. for i, existing := range mgr.staticRoutes {
  149. if normalizeDst(existing.DstAddress) == normOldDst && existing.RoutingTable == editReq2.OldRoutingTable {
  150. if existing.Gateway == editReq2.OldGateway || existing.Interface == editReq2.OldGateway {
  151. foundIndex = i
  152. break
  153. }
  154. }
  155. }
  156. if foundIndex != 0 {
  157. t.Fatalf("expected foundIndex 0 from OldDst/OldGw match, got %d", foundIndex)
  158. }
  159. // Preserve ID
  160. editReq2.ID = mgr.staticRoutes[foundIndex].ID
  161. mgr.staticRoutes[foundIndex] = editReq2
  162. if len(mgr.staticRoutes) != 1 {
  163. t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes))
  164. }
  165. if mgr.staticRoutes[0].ID != "route-100" {
  166. t.Errorf("expected ID route-100 preserved, got %s", mgr.staticRoutes[0].ID)
  167. }
  168. if mgr.staticRoutes[0].Gateway != "10.42.36.252" {
  169. t.Errorf("expected gateway 10.42.36.252, got %s", mgr.staticRoutes[0].Gateway)
  170. }
  171. if mgr.staticRoutes[0].CheckTarget != "10.22.11.4" {
  172. t.Errorf("expected check_target 10.22.11.4, got %s", mgr.staticRoutes[0].CheckTarget)
  173. }
  174. }
  175. func TestDeleteRouteMatchingKrID(t *testing.T) {
  176. mgr := &RoutingManager{
  177. staticRoutes: []RouteItem{
  178. {
  179. ID: "route-100",
  180. DstAddress: "10.0.0.0/8",
  181. Gateway: "10.42.36.254",
  182. Interface: "AWMN-Internal",
  183. RoutingTable: "main",
  184. Distance: 1,
  185. },
  186. {
  187. ID: "route-200",
  188. DstAddress: "0.0.0.0/0",
  189. Gateway: "sdwan0",
  190. Interface: "sdwan0",
  191. RoutingTable: "main",
  192. Distance: 1,
  193. },
  194. },
  195. }
  196. // Simulating a live route from kernel corresponding to route-100
  197. liveRoute := RouteItem{
  198. ID: "kr-254-10.0.0.0_8-10.42.36.254",
  199. DstAddress: "10.0.0.0/8",
  200. Gateway: "10.42.36.254",
  201. Interface: "AWMN-Internal",
  202. RoutingTable: "main",
  203. Distance: 1,
  204. }
  205. // If user clicks delete on kr-254-10.0.0.0_8-10.42.36.254
  206. idToDelete := liveRoute.ID
  207. targetIdx := -1
  208. for i, r := range mgr.staticRoutes {
  209. if r.ID == idToDelete {
  210. targetIdx = i
  211. break
  212. }
  213. }
  214. if targetIdx == -1 {
  215. // Fallback to matching live route against static routes
  216. normLrDst := normalizeDst(liveRoute.DstAddress)
  217. for i, sr := range mgr.staticRoutes {
  218. gwMatches := sr.Gateway == "" || liveRoute.Gateway == sr.Gateway || liveRoute.Interface == sr.Gateway
  219. if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == liveRoute.RoutingTable && gwMatches {
  220. targetIdx = i
  221. break
  222. }
  223. }
  224. }
  225. if targetIdx != 0 {
  226. t.Fatalf("expected targetIdx 0, got %d", targetIdx)
  227. }
  228. // Remove target
  229. mgr.staticRoutes = append(mgr.staticRoutes[:targetIdx], mgr.staticRoutes[targetIdx+1:]...)
  230. if len(mgr.staticRoutes) != 1 {
  231. t.Fatalf("expected 1 route remaining, got %d", len(mgr.staticRoutes))
  232. }
  233. if mgr.staticRoutes[0].ID != "route-200" {
  234. t.Errorf("expected route-200 to remain, got %s", mgr.staticRoutes[0].ID)
  235. }
  236. }
  237. func TestNormalizeDst(t *testing.T) {
  238. if normalizeDst("default") != "default" {
  239. t.Errorf("expected default, got %s", normalizeDst("default"))
  240. }
  241. if normalizeDst("0.0.0.0/0") != "default" {
  242. t.Errorf("expected default, got %s", normalizeDst("0.0.0.0/0"))
  243. }
  244. if normalizeDst("") != "default" {
  245. t.Errorf("expected default, got %s", normalizeDst(""))
  246. }
  247. if normalizeDst("10.0.0.0/8") != "10.0.0.0/8" {
  248. t.Errorf("expected 10.0.0.0/8, got %s", normalizeDst("10.0.0.0/8"))
  249. }
  250. }
  251. func TestSaveAndDeleteRouteLifecycle(t *testing.T) {
  252. tmpDir := t.TempDir()
  253. origStore := StaticRoutesStorePath
  254. defer func() { StaticRoutesStorePath = origStore }()
  255. StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
  256. mgr := &RoutingManager{
  257. staticRoutes: make([]RouteItem, 0),
  258. }
  259. // 1. Save new route with empty ID
  260. r1 := RouteItem{
  261. DstAddress: "198.51.100.0/24",
  262. Gateway: "blackhole",
  263. Action: "blackhole",
  264. RoutingTable: "main",
  265. Distance: 10,
  266. Comment: "Initial route",
  267. }
  268. if err := mgr.SaveRoute(&r1); err != nil {
  269. t.Fatalf("expected nil error on SaveRoute, got %v", err)
  270. }
  271. if r1.ID == "" {
  272. t.Fatalf("expected SaveRoute to populate r1.ID, got empty")
  273. }
  274. if len(mgr.staticRoutes) != 1 {
  275. t.Fatalf("expected 1 static route in manager, got %d", len(mgr.staticRoutes))
  276. }
  277. savedID := r1.ID
  278. // 2. Edit existing route with populated ID
  279. r1Edit := RouteItem{
  280. ID: savedID,
  281. DstAddress: "198.51.100.0/24",
  282. Gateway: "blackhole",
  283. Action: "blackhole",
  284. RoutingTable: "main",
  285. Distance: 20,
  286. Comment: "Updated route",
  287. }
  288. if err := mgr.SaveRoute(&r1Edit); err != nil {
  289. t.Fatalf("expected nil error on editing SaveRoute, got %v", err)
  290. }
  291. if len(mgr.staticRoutes) != 1 {
  292. t.Fatalf("expected still 1 static route after edit, got %d", len(mgr.staticRoutes))
  293. }
  294. if mgr.staticRoutes[0].ID != savedID {
  295. t.Errorf("expected ID %s preserved, got %s", savedID, mgr.staticRoutes[0].ID)
  296. }
  297. if mgr.staticRoutes[0].Distance != 20 {
  298. t.Errorf("expected distance 20, got %d", mgr.staticRoutes[0].Distance)
  299. }
  300. if mgr.staticRoutes[0].Comment != "Updated route" {
  301. t.Errorf("expected comment 'Updated route', got %s", mgr.staticRoutes[0].Comment)
  302. }
  303. // 3. Delete route
  304. delErr := mgr.DeleteRoute(savedID)
  305. if delErr != nil {
  306. t.Fatalf("expected nil error on DeleteRoute, got %v", delErr)
  307. }
  308. if len(mgr.staticRoutes) != 0 {
  309. t.Fatalf("expected 0 static routes after delete, got %d", len(mgr.staticRoutes))
  310. }
  311. }
  312. func TestMultipathMixedNexthopFlags(t *testing.T) {
  313. tmpDir := t.TempDir()
  314. origStore := StaticRoutesStorePath
  315. defer func() { StaticRoutesStorePath = origStore }()
  316. StaticRoutesStorePath = filepath.Join(tmpDir, "static_routes.json")
  317. mgr := &RoutingManager{
  318. staticRoutes: make([]RouteItem, 0),
  319. }
  320. // Route 1: via IP with device (hasVia = true)
  321. r1 := RouteItem{
  322. DstAddress: "198.51.100.0/24",
  323. Gateway: "100.68.0.1",
  324. Interface: "Fibergrid",
  325. RoutingTable: "main",
  326. Distance: 10,
  327. }
  328. if err := mgr.SaveRoute(&r1); err != nil {
  329. t.Fatalf("failed to save route 1: %v", err)
  330. }
  331. // Route 2: device only without gateway IP (hasVia = false, e.g. sdwan0-Internet)
  332. r2 := RouteItem{
  333. DstAddress: "198.51.100.0/24",
  334. Gateway: "sdwan0-Internet",
  335. Interface: "sdwan0-Internet",
  336. RoutingTable: "main",
  337. Distance: 10,
  338. }
  339. // This previously threw "Error: Invalid flags for nexthop - PERVASIVE and ONLINK can not be set."
  340. if err := mgr.SaveRoute(&r2); err != nil {
  341. t.Fatalf("failed to save multipath route 2: %v", err)
  342. }
  343. if len(mgr.staticRoutes) != 2 {
  344. t.Fatalf("expected 2 routes in multipath group, got %d", len(mgr.staticRoutes))
  345. }
  346. // Cleanup
  347. _ = mgr.DeleteRoute(r1.ID)
  348. _ = mgr.DeleteRoute(r2.ID)
  349. }