package modernzconf import ( "os" "path/filepath" "runtime" "strings" "testing" ) // sampleConf is a representative excerpt of the real default modernz.conf // shipped by ModernZ (github.com/Samillion/ModernZ). const sampleConf = `# Language and display # set language (for available options, see: https://github.com/Samillion/ModernZ/blob/main/docs/TRANSLATIONS.md) language=default # set layout: default, compact, mini, seekbar layout=default # show mpv logo when idle idlescreen=yes # OSC behaviour and scaling # time (in ms) before OSC hides if no mouse movement hidetimeout=1500 # scale osc with the video vidscale=auto # osc scale factor when windowed scalewindowed=1.0 # osc scale factor when fullscreen scalefullscreen=1.0 # accent color of the OSC and title bar osc_color=#000000 # color of the seekbar progress seekbarfg_color=#FF8232 # seekbar height preset: small, medium, large, xlarge seekbar_height=medium # show the speed control button speed_button=yes ` // writeSample writes sampleConf to a temp file and returns its path. func writeSample(t *testing.T) string { t.Helper() path := filepath.Join(t.TempDir(), "modernz.conf") if err := os.WriteFile(path, []byte(sampleConf), 0o644); err != nil { t.Fatalf("failed to write sample conf: %v", err) } return path } func TestParseModernZConf(t *testing.T) { conf, err := ParseModernZConf(writeSample(t)) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } // Explicitly set values if !conf.IsSet("scalewindowed") { t.Error("expected scalewindowed to be set") } if got := conf.GetValue("scalewindowed"); got != "1.0" { t.Errorf("GetValue(scalewindowed) = %q, want %q", got, "1.0") } if got := conf.GetValue("hidetimeout"); got != "1500" { t.Errorf("GetValue(hidetimeout) = %q, want %q", got, "1500") } // Hex color values must parse whole, not as an inline comment if got := conf.GetValue("osc_color"); got != "#000000" { t.Errorf("GetValue(osc_color) = %q, want %q", got, "#000000") } // Unset known option falls back to its default if conf.IsSet("showwindowed") { t.Error("expected showwindowed to not be set") } if got := conf.GetValue("showwindowed"); got != "yes" { t.Errorf("GetValue(showwindowed) = %q, want default %q", got, "yes") } // Unknown option if got := conf.GetValue("not_a_real_option"); got != "" { t.Errorf("GetValue(unknown) = %q, want %q", got, "") } } func TestParseModernZConfMissingFile(t *testing.T) { path := filepath.Join(t.TempDir(), "script-opts", "modernz.conf") conf, err := ParseModernZConf(path) if err != nil { t.Fatalf("missing file should not be an error, got: %v", err) } // Populated from defaults if got := conf.GetValue("layout"); got != "default" { t.Errorf("GetValue(layout) = %q, want default %q", got, "default") } if conf.IsSet("layout") { t.Error("expected layout to not be set") } // Set + Write must create the missing parent directory if err := conf.SetValue("seekbar_height", "large"); err != nil { t.Fatalf("SetValue failed: %v", err) } if err := conf.Write(); err != nil { t.Fatalf("Write failed: %v", err) } reloaded, err := ParseModernZConf(path) if err != nil { t.Fatalf("re-parse failed: %v", err) } if got := reloaded.GetValue("seekbar_height"); got != "large" { t.Errorf("GetValue(seekbar_height) = %q, want %q", got, "large") } } func TestSetResetWriteRoundTrip(t *testing.T) { path := writeSample(t) conf, err := ParseModernZConf(path) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } // Update an existing option and add a previously unset one if err := conf.SetValue("scalewindowed", "1.5"); err != nil { t.Fatalf("SetValue(scalewindowed) failed: %v", err) } if err := conf.SetValue("showwindowed", "no"); err != nil { t.Fatalf("SetValue(showwindowed) failed: %v", err) } if err := conf.Write(); err != nil { t.Fatalf("Write failed: %v", err) } reloaded, err := ParseModernZConf(path) if err != nil { t.Fatalf("re-parse failed: %v", err) } if got := reloaded.GetValue("scalewindowed"); got != "1.5" { t.Errorf("GetValue(scalewindowed) = %q, want %q", got, "1.5") } if got := reloaded.GetValue("showwindowed"); got != "no" { t.Errorf("GetValue(showwindowed) = %q, want %q", got, "no") } if !reloaded.IsSet("showwindowed") { t.Error("expected showwindowed to be set after write") } // Reset reverts to the default and drops the explicit line if err := reloaded.ResetValue("showwindowed"); err != nil { t.Fatalf("ResetValue failed: %v", err) } if err := reloaded.Write(); err != nil { t.Fatalf("second Write failed: %v", err) } final, err := ParseModernZConf(path) if err != nil { t.Fatalf("final parse failed: %v", err) } if final.IsSet("showwindowed") { t.Error("expected showwindowed to be unset after reset") } if got := final.GetValue("showwindowed"); got != "yes" { t.Errorf("GetValue(showwindowed) = %q, want default %q", got, "yes") } if got := final.GetValue("scalewindowed"); got != "1.5" { t.Errorf("GetValue(scalewindowed) = %q, want %q", got, "1.5") } } func TestSetValueRejectsInvalid(t *testing.T) { conf, err := ParseModernZConf(writeSample(t)) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } if err := conf.SetValue("scalewindowed", "99"); err == nil { t.Error("expected out-of-range SetValue to fail") } if err := conf.SetValue("bogus_key", "x"); err == nil { t.Error("expected unknown-key SetValue to fail") } if got := conf.GetValue("scalewindowed"); got != "1.0" { t.Errorf("failed SetValue mutated conf: GetValue = %q, want %q", got, "1.0") } if err := conf.ResetValue("bogus_key"); err == nil { t.Error("expected unknown-key ResetValue to fail") } } func TestManagedInstallDefaults(t *testing.T) { path := writeSample(t) if err := ApplyManagedInstallDefaults(path, nil); err != nil { t.Fatalf("ApplyManagedInstallDefaults failed: %v", err) } conf, err := ParseModernZConf(path) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } for key, want := range map[string]string{ "vidscale": ManagedVidscaleDefault, "scalewindowed": ManagedScaleWindowedDefault, "scalefullscreen": ManagedScaleFullscreenDefault, } { if got := conf.GetValue(key); got != want { t.Errorf("GetValue(%q) = %q, want managed default %q", key, got, want) } } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read managed config: %v", err) } content := string(data) if !strings.Contains(content, "# OSC behaviour and scaling") { t.Error("managed default rewrite did not preserve upstream comments") } if !strings.Contains(content, "layout=default") { t.Error("managed default rewrite did not preserve unrelated options") } backups, err := filepath.Glob(path + "-modernz.conf.bak" + ".*") if err != nil { t.Fatalf("failed to inspect backups: %v", err) } if len(backups) != 0 { t.Errorf("staged install default rewrite created unexpected backups: %v", backups) } } func TestManagedInstallDefaultsPreserveExistingScaling(t *testing.T) { existingPath := writeSample(t) existing, err := ParseModernZConf(existingPath) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } for key, value := range map[string]string{ "vidscale": "yes", "scalewindowed": "1.5", "scalefullscreen": "1.75", } { if err := existing.SetValue(key, value); err != nil { t.Fatalf("SetValue(%q) failed: %v", key, err) } } if err := existing.Write(); err != nil { t.Fatalf("failed to write existing config: %v", err) } preserved, err := ManagedScalingValues(existingPath) if err != nil { t.Fatalf("ManagedScalingValues failed: %v", err) } stagedPath := filepath.Join(t.TempDir(), "modernz.conf") if err := os.WriteFile(stagedPath, []byte(sampleConf), 0o644); err != nil { t.Fatalf("failed to write staged config: %v", err) } if err := ApplyManagedInstallDefaults(stagedPath, preserved); err != nil { t.Fatalf("ApplyManagedInstallDefaults failed: %v", err) } staged, err := ParseModernZConf(stagedPath) if err != nil { t.Fatalf("failed to parse prepared config: %v", err) } for key, want := range preserved { if got := staged.GetValue(key); got != want { t.Errorf("preserved %s = %q, want %q", key, got, want) } } } func TestScalingOptionsHaveFriendlyMetadata(t *testing.T) { wants := map[string]struct { label string recommended string }{ "vidscale": {label: "Scaling behavior", recommended: ManagedVidscaleDefault}, "scalewindowed": {label: "Windowed UI size", recommended: ManagedScaleWindowedDefault}, "scalefullscreen": {label: "Fullscreen UI size", recommended: ManagedScaleFullscreenDefault}, } for _, opt := range KnownOptions() { want, ok := wants[opt.Key] if !ok { continue } if opt.Label != want.label { t.Errorf("%s label = %q, want %q", opt.Key, opt.Label, want.label) } if opt.Recommended != want.recommended { t.Errorf("%s recommended = %q, want %q", opt.Key, opt.Recommended, want.recommended) } delete(wants, opt.Key) } if len(wants) != 0 { t.Errorf("missing scaling option metadata for: %v", wants) } } func TestCommentAndStructurePreservation(t *testing.T) { path := writeSample(t) conf, err := ParseModernZConf(path) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } if err := conf.SetValue("hidetimeout", "2000"); err != nil { t.Fatalf("SetValue failed: %v", err) } if err := conf.Write(); err != nil { t.Fatalf("Write failed: %v", err) } data, err := os.ReadFile(path) if err != nil { t.Fatalf("failed to read written conf: %v", err) } content := string(data) // Comments and untouched lines survive verbatim for _, want := range []string{ "# Language and display", "# time (in ms) before OSC hides if no mouse movement", "# accent color of the OSC and title bar", "osc_color=#000000", "vidscale=auto", "scalefullscreen=1.0", "seekbar_height=medium", } { if !strings.Contains(content, want) { t.Errorf("written conf lost line %q", want) } } // The edited line is replaced, nothing else changes if !strings.Contains(content, "hidetimeout=2000") { t.Error("written conf missing updated hidetimeout=2000") } if strings.Contains(content, "hidetimeout=1500") { t.Error("written conf still contains old hidetimeout=1500") } if got, want := len(strings.Split(content, "\n")), len(strings.Split(sampleConf, "\n")); got != want { t.Errorf("line count changed: got %d, want %d", got, want) } } func TestWritePreservesBOMCRLFAndMode(t *testing.T) { path := filepath.Join(t.TempDir(), "modernz.conf") original := []byte("\xef\xbb\xbfvidscale=auto\r\nhidetimeout=1500 # keep\r\n") if err := os.WriteFile(path, original, 0o600); err != nil { t.Fatal(err) } conf, err := ParseModernZConf(path) if err != nil { t.Fatal(err) } if err := conf.SetValue("hidetimeout", "2000"); err != nil { t.Fatal(err) } if err := conf.Write(); err != nil { t.Fatal(err) } written, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if !strings.HasPrefix(string(written), "\ufeff") { t.Fatal("UTF-8 BOM was lost") } withoutCRLF := strings.ReplaceAll(string(written), "\r\n", "") if strings.ContainsAny(withoutCRLF, "\r\n") { t.Fatalf("newline style was mixed: %q", written) } if !strings.Contains(string(written), "hidetimeout=2000 # keep\r\n") { t.Fatalf("edited line/comment not preserved: %q", written) } info, err := os.Stat(path) if err != nil { t.Fatal(err) } if got := info.Mode().Perm(); got != 0o600 { t.Fatalf("mode = %o, want 600", got) } } func TestParseAndWriteRejectSymlink(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("symlink creation requires elevated privileges on Windows") } dir := t.TempDir() target := filepath.Join(dir, "target.conf") link := filepath.Join(dir, "modernz.conf") if err := os.WriteFile(target, []byte("vidscale=auto\n"), 0o600); err != nil { t.Fatal(err) } if err := os.Symlink(target, link); err != nil { t.Fatal(err) } if _, err := ParseModernZConf(link); err == nil { t.Fatal("symlinked script config was accepted") } if err := SetValueFile(link, "vidscale", "no"); err == nil { t.Fatal("symlinked script config was replaced") } content, err := os.ReadFile(target) if err != nil { t.Fatal(err) } if string(content) != "vidscale=auto\n" { t.Fatalf("symlink target changed: %q", content) } } func TestBackupCreatedOnSecondWrite(t *testing.T) { path := writeSample(t) conf, err := ParseModernZConf(path) if err != nil { t.Fatalf("ParseModernZConf failed: %v", err) } if err := conf.SetValue("hidetimeout", "2000"); err != nil { t.Fatalf("SetValue failed: %v", err) } if err := conf.Write(); err != nil { t.Fatalf("first Write failed: %v", err) } if err := conf.SetValue("hidetimeout", "3000"); err != nil { t.Fatalf("SetValue failed: %v", err) } if err := conf.Write(); err != nil { t.Fatalf("second Write failed: %v", err) } backups, err := filepath.Glob(path + "-modernz.conf.bak" + ".*") if err != nil { t.Fatalf("backup glob failed: %v", err) } if len(backups) != 2 { t.Fatalf("expected every rapid write to preserve a distinct backup, got %d", len(backups)) } priorValues := map[string]bool{"hidetimeout=1500": false, "hidetimeout=2000": false} for _, backupPath := range backups { backup, err := os.ReadFile(backupPath) if err != nil { t.Fatalf("failed to read backup: %v", err) } backupContent := string(backup) for value := range priorValues { if strings.Contains(backupContent, value) { priorValues[value] = true } } if strings.Contains(backupContent, "hidetimeout=3000") { t.Error("backup contains the final content instead of a prior state") } } for value, found := range priorValues { if !found { t.Errorf("distinct backups did not preserve %s", value) } } } func TestValidateValue(t *testing.T) { tests := []struct { name string key string value string wantErr bool }{ {"bool yes", "showwindowed", "yes", false}, {"bool no", "showwindowed", "no", false}, {"bool true rejected", "showwindowed", "true", true}, {"bool 1 rejected", "showwindowed", "1", true}, {"int valid", "hidetimeout", "1500", false}, {"int min bound ok", "hidetimeout", "0", false}, {"int below min", "hidetimeout", "-1", true}, {"int above max", "hidetimeout", "60001", true}, {"int not a number", "hidetimeout", "abc", true}, {"int float rejected", "hidetimeout", "1.5", true}, {"float valid", "scalewindowed", "2.5", false}, {"float default ok", "scalefullscreen", "1.0", false}, {"float below min", "scalewindowed", "0.05", true}, {"float above max", "scalewindowed", "5.1", true}, {"float not a number", "scalewindowed", "big", true}, {"float nan rejected", "scalewindowed", "NaN", true}, {"choice valid", "layout", "compact", false}, {"choice invalid", "layout", "weird", true}, {"choice case sensitive", "seekbar_height", "Medium", true}, {"string anything", "language", "en", false}, {"string hex color", "osc_color", "#123456", false}, {"unknown key", "definitely_not_real", "x", true}, {"newline injection", "language", "en\nhacked=1", true}, {"crlf injection", "osc_color", "#fff\r\nhacked=1", true}, {"newline in unknown key value", "definitely_not_real", "a\nb", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := ValidateValue(tt.key, tt.value) if tt.wantErr && err == nil { t.Errorf("ValidateValue(%q, %q) = nil, want error", tt.key, tt.value) } if !tt.wantErr && err != nil { t.Errorf("ValidateValue(%q, %q) = %v, want nil", tt.key, tt.value, err) } }) } } func TestKnownOptionsDefaultsValid(t *testing.T) { opts := KnownOptions() if len(opts) == 0 { t.Fatal("KnownOptions returned no options") } for _, opt := range opts { if opt.Key == "" || opt.Description == "" { t.Errorf("option %+v missing key or description", opt) } if err := ValidateValue(opt.Key, opt.Default); err != nil { t.Errorf("default %q for %q fails validation: %v", opt.Default, opt.Key, err) } if opt.ValueType == TypeChoice && len(opt.Choices) == 0 { t.Errorf("choice option %q has no choices", opt.Key) } } } func TestGetModernZConfPath(t *testing.T) { path, err := GetModernZConfPath() if err != nil { t.Skipf("home directory not available: %v", err) } wantSuffix := filepath.Join("script-opts", "modernz.conf") if !strings.HasSuffix(path, wantSuffix) { t.Errorf("GetModernZConfPath() = %q, want suffix %q", path, wantSuffix) } }