// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package preferences import ( "fmt" "reflect" "strings" "testing" "git.stormux.org/storm/cthulhu/internal/config" ) func TestWineValuesRoundTripAndOwnership(t *testing.T) { settings := config.DefaultSettings() settings.Wine = config.WineSettings{ControllerOnly: true, ExcludedPrograms: []string{"Game.exe", "Another Game.exe"}} values := FromSettings(settings) if !reflect.DeepEqual(values.Wine, settings.Wine) { t.Fatalf("Wine values = %+v", values.Wine) } values.Wine.ExcludedPrograms[0] = "Changed.exe" if settings.Wine.ExcludedPrograms[0] != "Game.exe" { t.Fatal("FromSettings aliases the Wine program list") } values.Wine.ExcludedPrograms[0] = "Game.exe" values.Wine.ControllerOnly = false values.Wine.ExcludedPrograms = append(values.Wine.ExcludedPrograms, "New Game.exe") merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } if got := FromSettings(merged); !reflect.DeepEqual(got, values) { t.Fatalf("Wine values did not round trip:\n got %+v\nwant %+v", got.Wine, values.Wine) } values.Wine.ExcludedPrograms[0] = "Changed.exe" if merged.Wine.ExcludedPrograms[0] != "Game.exe" { t.Fatal("merged Wine programs alias the UI values") } } func TestWineValuesMergeCollapsesCaseInsensitiveDuplicates(t *testing.T) { settings := config.DefaultSettings() values := FromSettings(settings) values.Wine.ExcludedPrograms = []string{"Game.exe", "game.EXE", "Other.exe", "GAME.exe", "other.exe"} merged, err := values.Merge(settings) if err != nil { t.Fatal(err) } want := []string{"Game.exe", "Other.exe"} if !reflect.DeepEqual(merged.Wine.ExcludedPrograms, want) { t.Fatalf("collapsed Wine programs = %q, want %q", merged.Wine.ExcludedPrograms, want) } values.Wine.ExcludedPrograms = nil merged, err = values.Merge(settings) if err != nil { t.Fatal(err) } if merged.Wine.ExcludedPrograms != nil { t.Fatalf("empty Wine list became %q, want nil", merged.Wine.ExcludedPrograms) } } func TestWineValuesMergeRejectsInvalidEntriesByName(t *testing.T) { overlong := strings.Repeat("a", 252) + ".exe" cases := []struct { name string entry string wantSub string }{ {name: "windows path", entry: `C:\Games\Game.exe`, wantSub: `C:\Games\Game.exe`}, {name: "unix path", entry: "Games/Game.exe", wantSub: "Games/Game.exe"}, {name: "blank", entry: "", wantSub: "blank"}, {name: "spaces only", entry: " ", wantSub: "blank"}, {name: "overlong", entry: overlong, wantSub: overlong}, {name: "leading space", entry: " Game.exe", wantSub: "Game.exe"}, {name: "trailing space", entry: "Game.exe ", wantSub: "Game.exe"}, } for _, test := range cases { t.Run(test.name, func(t *testing.T) { settings := config.DefaultSettings() settings.Wine.ExcludedPrograms = []string{"Kept.exe"} values := FromSettings(settings) values.Wine.ExcludedPrograms = []string{"Kept.exe", test.entry} merged, err := values.Merge(settings) if err == nil { t.Fatalf("Merge accepted Wine entry %q", test.entry) } if !strings.Contains(err.Error(), test.wantSub) { t.Fatalf("Merge error %q does not name %q", err, test.wantSub) } if !reflect.DeepEqual(merged.Wine.ExcludedPrograms, []string{"Kept.exe"}) { t.Fatalf("rejected Merge changed Wine programs to %q", merged.Wine.ExcludedPrograms) } }) } } func TestWineValuesRejectEmbeddedControlCharacters(t *testing.T) { for character := rune(0); character < ' '; character++ { t.Run(fmt.Sprintf("U+%04X", character), func(t *testing.T) { settings := config.DefaultSettings() settings.Wine.ExcludedPrograms = []string{"Kept.exe"} values := FromSettings(settings) entry := "Game" + string(character) + ".exe" values.Wine.ExcludedPrograms = []string{entry} merged, err := values.Merge(settings) if err == nil || !strings.Contains(err.Error(), entry) { t.Fatalf("invalid entry was not named and rejected: %v", err) } if !reflect.DeepEqual(merged.Wine, settings.Wine) { t.Fatalf("rejected entry changed Wine settings: %+v", merged.Wine) } }) } }