package routing import ( "encoding/json" "os" "path/filepath" "testing" ) func TestDeduplicationOnLoad(t *testing.T) { tmpDir := t.TempDir() storePath := filepath.Join(tmpDir, "static_routes.json") // Create test JSON with duplicate routes routes := []RouteItem{ { ID: "route-1", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.254", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, Comment: "Original", }, { ID: "route-2", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.254", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, Comment: "Duplicate", }, { ID: "route-3", DstAddress: "192.168.1.0/24", Gateway: "192.168.1.1", RoutingTable: "main", Distance: 1, }, } data, err := json.Marshal(routes) if err != nil { t.Fatalf("failed to marshal routes: %v", err) } if err := os.WriteFile(storePath, data, 0600); err != nil { t.Fatalf("failed to write test file: %v", err) } mgr := &RoutingManager{ staticRoutes: make([]RouteItem, 0), } // Read and deduplicate using the exact logic from load() seenIDs := make(map[string]bool) seenKeys := make(map[string]bool) var dedupedRev []RouteItem hasDups := false var loadedRoutes []RouteItem _ = json.Unmarshal(data, &loadedRoutes) for i := len(loadedRoutes) - 1; i >= 0; i-- { r := loadedRoutes[i] normDst := normalizeDst(r.DstAddress) tbl := r.RoutingTable if tbl == "" { tbl = "main" } routeKey := normDst + "|" + tbl + "|" + r.Gateway + "|" + r.Interface + "|1" if seenIDs[r.ID] || seenKeys[routeKey] { hasDups = true continue } seenIDs[r.ID] = true seenKeys[routeKey] = true dedupedRev = append(dedupedRev, r) } var deduped []RouteItem for i := len(dedupedRev) - 1; i >= 0; i-- { deduped = append(deduped, dedupedRev[i]) } mgr.staticRoutes = deduped if !hasDups { t.Errorf("expected duplicates to be detected") } if len(mgr.staticRoutes) != 2 { t.Fatalf("expected 2 routes after dedup, got %d", len(mgr.staticRoutes)) } // The latest entry for 10.0.0.0/8 should have been preserved if mgr.staticRoutes[0].ID != "route-2" || mgr.staticRoutes[0].Comment != "Duplicate" { t.Errorf("expected route-2 (latest) to be kept, got %+v", mgr.staticRoutes[0]) } } func TestSaveRouteUpdateNoDuplicates(t *testing.T) { mgr := &RoutingManager{ staticRoutes: []RouteItem{ { ID: "route-100", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.254", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, Comment: "Original AWMN Route", }, }, } // 1. Edit using matching ID editReq1 := RouteItem{ ID: "route-100", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.253", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, Comment: "Updated Gateway", } // Test findIndex logic foundIndex := -1 for i, existing := range mgr.staticRoutes { if existing.ID == editReq1.ID { foundIndex = i break } } if foundIndex != 0 { t.Fatalf("expected foundIndex 0, got %d", foundIndex) } mgr.staticRoutes[foundIndex] = editReq1 if len(mgr.staticRoutes) != 1 { t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes)) } if mgr.staticRoutes[0].Gateway != "10.42.36.253" { t.Errorf("expected gateway 10.42.36.253, got %s", mgr.staticRoutes[0].Gateway) } // 2. Edit where UI sent kr-... ID (kernel route adoption/edit) but with OldDstAddress and OldGateway editReq2 := RouteItem{ ID: "kr-254-10.0.0.0_8-10.42.36.253", OldDstAddress: "10.0.0.0/8", OldGateway: "10.42.36.253", OldRoutingTable: "main", OldInterface: "AWMN-Internal", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.252", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, CheckGateway: "internet", CheckTarget: "10.22.11.4", } foundIndex = -1 // Match by OldDstAddress / OldGateway normOldDst := normalizeDst(editReq2.OldDstAddress) for i, existing := range mgr.staticRoutes { if normalizeDst(existing.DstAddress) == normOldDst && existing.RoutingTable == editReq2.OldRoutingTable { if existing.Gateway == editReq2.OldGateway || existing.Interface == editReq2.OldGateway { foundIndex = i break } } } if foundIndex != 0 { t.Fatalf("expected foundIndex 0 from OldDst/OldGw match, got %d", foundIndex) } // Preserve ID editReq2.ID = mgr.staticRoutes[foundIndex].ID mgr.staticRoutes[foundIndex] = editReq2 if len(mgr.staticRoutes) != 1 { t.Fatalf("expected 1 route, got %d (duplicate created!)", len(mgr.staticRoutes)) } if mgr.staticRoutes[0].ID != "route-100" { t.Errorf("expected ID route-100 preserved, got %s", mgr.staticRoutes[0].ID) } if mgr.staticRoutes[0].Gateway != "10.42.36.252" { t.Errorf("expected gateway 10.42.36.252, got %s", mgr.staticRoutes[0].Gateway) } if mgr.staticRoutes[0].CheckTarget != "10.22.11.4" { t.Errorf("expected check_target 10.22.11.4, got %s", mgr.staticRoutes[0].CheckTarget) } } func TestDeleteRouteMatchingKrID(t *testing.T) { mgr := &RoutingManager{ staticRoutes: []RouteItem{ { ID: "route-100", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.254", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, }, { ID: "route-200", DstAddress: "0.0.0.0/0", Gateway: "sdwan0", Interface: "sdwan0", RoutingTable: "main", Distance: 1, }, }, } // Simulating a live route from kernel corresponding to route-100 liveRoute := RouteItem{ ID: "kr-254-10.0.0.0_8-10.42.36.254", DstAddress: "10.0.0.0/8", Gateway: "10.42.36.254", Interface: "AWMN-Internal", RoutingTable: "main", Distance: 1, } // If user clicks delete on kr-254-10.0.0.0_8-10.42.36.254 idToDelete := liveRoute.ID targetIdx := -1 for i, r := range mgr.staticRoutes { if r.ID == idToDelete { targetIdx = i break } } if targetIdx == -1 { // Fallback to matching live route against static routes normLrDst := normalizeDst(liveRoute.DstAddress) for i, sr := range mgr.staticRoutes { gwMatches := sr.Gateway == "" || liveRoute.Gateway == sr.Gateway || liveRoute.Interface == sr.Gateway if normalizeDst(sr.DstAddress) == normLrDst && sr.RoutingTable == liveRoute.RoutingTable && gwMatches { targetIdx = i break } } } if targetIdx != 0 { t.Fatalf("expected targetIdx 0, got %d", targetIdx) } // Remove target mgr.staticRoutes = append(mgr.staticRoutes[:targetIdx], mgr.staticRoutes[targetIdx+1:]...) if len(mgr.staticRoutes) != 1 { t.Fatalf("expected 1 route remaining, got %d", len(mgr.staticRoutes)) } if mgr.staticRoutes[0].ID != "route-200" { t.Errorf("expected route-200 to remain, got %s", mgr.staticRoutes[0].ID) } } func TestNormalizeDst(t *testing.T) { if normalizeDst("default") != "default" { t.Errorf("expected default, got %s", normalizeDst("default")) } if normalizeDst("0.0.0.0/0") != "default" { t.Errorf("expected default, got %s", normalizeDst("0.0.0.0/0")) } if normalizeDst("") != "default" { t.Errorf("expected default, got %s", normalizeDst("")) } if normalizeDst("10.0.0.0/8") != "10.0.0.0/8" { t.Errorf("expected 10.0.0.0/8, got %s", normalizeDst("10.0.0.0/8")) } }