package preset import ( "os" "path/filepath" "strings" "testing" ) func newTestStore(t *testing.T) *Store { t.Helper() s, err := NewStore(filepath.Join(t.TempDir(), "presets.json")) if err != nil { t.Fatal(err) } return s } func sample() Preset { return Preset{ Name: "Artist frame — control", Group: "Riedel", Adapter: "Ethernet", Mode: ModeStatic, Primary: &Address{Address: "192.168.42.100", Prefix: 24, Gateway: "192.168.42.1"}, DNS: DNS{Mode: ModeStatic, Servers: []string{"192.168.42.1"}}, } } // The shipped example doubles as the documented export format, so a change // that breaks it breaks every preset pack already in circulation. func TestExamplePackDecodes(t *testing.T) { b, err := os.ReadFile(filepath.Join("..", "..", "examples", "preset-pack.json")) if err != nil { t.Skipf("example pack not present: %v", err) } f, err := Decode(b) if err != nil { t.Fatalf("examples/preset-pack.json does not validate: %v", err) } if len(f.Presets) == 0 { t.Fatal("example pack is empty") } } func TestMissingFileStartsEmpty(t *testing.T) { s := newTestStore(t) if len(s.All()) != 0 { t.Errorf("a fresh store should have no presets, got %d", len(s.All())) } } func TestPutAssignsIDAndPersists(t *testing.T) { path := filepath.Join(t.TempDir(), "presets.json") s, err := NewStore(path) if err != nil { t.Fatal(err) } saved, err := s.Put(sample()) if err != nil { t.Fatal(err) } if saved.ID == "" { t.Fatal("Put should have assigned an id") } reopened, err := NewStore(path) if err != nil { t.Fatalf("saved file did not reload: %v", err) } if got, ok := reopened.Get(saved.ID); !ok || got.Name != saved.Name { t.Errorf("preset did not survive a reload: %+v", got) } } func TestPutRejectsInvalid(t *testing.T) { s := newTestStore(t) bad := sample() bad.Primary.Address = "not-an-ip" if _, err := s.Put(bad); err == nil { t.Error("expected a validation error for a bad address") } // A secondary carrying a gateway is silently dropped by netsh, so it is // rejected rather than saved as a lie. withGW := sample() withGW.Secondary = []Address{{Address: "10.0.0.5", Prefix: 24, Gateway: "10.0.0.1"}} if _, err := s.Put(withGW); err == nil { t.Error("expected a validation error for a secondary with a gateway") } } func TestDecodeRejectsWrongVersion(t *testing.T) { _, err := Decode([]byte(`{"version": 99, "presets": []}`)) if err == nil || !strings.Contains(err.Error(), "version") { t.Errorf("expected a version error, got %v", err) } } func TestDecodeRejectsDuplicateIDs(t *testing.T) { doc := `{"version":1,"presets":[ {"id":"a","name":"one","adapter":"Ethernet","mode":"dhcp","dns":{"mode":"dhcp"}}, {"id":"a","name":"two","adapter":"Ethernet","mode":"dhcp","dns":{"mode":"dhcp"}}]}` if _, err := Decode([]byte(doc)); err == nil { t.Error("expected a duplicate-id error") } } func TestImportMergeKeepsBothOnIDCollision(t *testing.T) { s := newTestStore(t) saved, err := s.Put(sample()) if err != nil { t.Fatal(err) } pack, err := s.Export() if err != nil { t.Fatal(err) } res, err := s.Import(pack, ImportMerge) if err != nil { t.Fatal(err) } if res.Added != 1 || len(res.Renamed) != 1 { t.Fatalf("expected 1 added and 1 renamed, got %+v", res) } if len(s.All()) != 2 { t.Fatalf("merge should keep both, got %d presets", len(s.All())) } // The original must be untouched: a colleague's pack silently overwriting // a working preset is the failure this guards against. orig, ok := s.Get(saved.ID) if !ok || orig.Name != saved.Name { t.Errorf("original preset was modified: %+v", orig) } var found bool for _, p := range s.All() { if strings.HasSuffix(p.Name, "(imported)") { found = true } } if !found { t.Error("the colliding import should have been suffixed with (imported)") } } func TestImportReplaceDiscardsExisting(t *testing.T) { s := newTestStore(t) if _, err := s.Put(sample()); err != nil { t.Fatal(err) } doc := `{"version":1,"presets":[{"id":"z","name":"only","adapter":"Ethernet","mode":"dhcp","dns":{"mode":"dhcp"}}]}` if _, err := s.Import([]byte(doc), ImportReplace); err != nil { t.Fatal(err) } all := s.All() if len(all) != 1 || all[0].ID != "z" { t.Errorf("replace should leave exactly the incoming presets, got %+v", all) } } // A rejected import must change nothing: partial application would leave the // user with a half-imported pack and no way to tell what landed. func TestImportRejectsWithoutMutating(t *testing.T) { s := newTestStore(t) if _, err := s.Put(sample()); err != nil { t.Fatal(err) } before := len(s.All()) bad := `{"version":1,"presets":[{"id":"x","name":"broken","adapter":"Ethernet","mode":"static","dns":{"mode":"dhcp"}}]}` if _, err := s.Import([]byte(bad), ImportMerge); err == nil { t.Fatal("expected the import to be rejected") } if len(s.All()) != before { t.Errorf("a rejected import changed the store: %d -> %d", before, len(s.All())) } } func TestExportFiltersByGroup(t *testing.T) { s := newTestStore(t) if _, err := s.Put(sample()); err != nil { t.Fatal(err) } other := sample() other.Name = "Management" other.Group = "Herespace" if _, err := s.Put(other); err != nil { t.Fatal(err) } b, err := s.Export("Riedel") if err != nil { t.Fatal(err) } f, err := Decode(b) if err != nil { t.Fatalf("a filtered export must still be a valid pack: %v", err) } if len(f.Presets) != 1 || f.Presets[0].Group != "Riedel" { t.Errorf("group filter returned %+v", f.Presets) } } func TestUngroupedSortsLast(t *testing.T) { s := newTestStore(t) loose := sample() loose.Name = "Loose" loose.Group = "" if _, err := s.Put(loose); err != nil { t.Fatal(err) } if _, err := s.Put(sample()); err != nil { t.Fatal(err) } groups := s.Groups() if len(groups) != 2 || groups[len(groups)-1] != "Ungrouped" { t.Errorf("Ungrouped should sort last, got %v", groups) } }