package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // A .json argument is a path and everything else is a name, and that is the whole // rule. The suffix is matched without regard to case so it reads the same on // Linux, macOS and Windows, and no separator is looked for: the Windows file API // takes both, so a check for one would be a platform difference with no behaviour // behind it. func TestResolveProfileTargetTreatsAJsonSuffixAsAPath(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) user := filepath.Join(userDir, "qmk-rgb-tool", "profiles") for _, tc := range []struct { arg string wantPath string wantName string wantFile bool }{ {"lala.json", "lala.json", "lala", true}, {"./lala.json", "./lala.json", "lala", true}, {"profiles/lava.json", "profiles/lava.json", "lava", true}, {"LALA.JSON", "LALA.JSON", "LALA", true}, {"lava", filepath.Join(user, "lava.json"), "lava", false}, {"my profile", filepath.Join(user, "my-profile.json"), "my profile", false}, } { path, name, isFile := resolveProfileTarget(tc.arg) if isFile != tc.wantFile { t.Errorf("resolveProfileTarget(%q) isPath = %v, want %v", tc.arg, isFile, tc.wantFile) } if path != tc.wantPath { t.Errorf("resolveProfileTarget(%q) path = %q, want %q", tc.arg, path, tc.wantPath) } if name != tc.wantName { t.Errorf("resolveProfileTarget(%q) name = %q, want %q", tc.arg, name, tc.wantName) } } } // The two forms read two different files, and the argument says which: a path // reads the file it names, a name reads the user's directory. The same fixture // holds a file of each, so a rule that guessed instead would have one answer for // both arguments. func TestLoadProfileReadsTheFileTheArgumentNames(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) here := t.TempDir() if err := os.MkdirAll(filepath.Join(here, "profiles"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(here, "profiles", "lava.json"), []byte(`{"name":"from-the-checkout","version":1,"zones":{}}`), 0o644); err != nil { t.Fatal(err) } dir := filepath.Join(userDir, "qmk-rgb-tool", "profiles") if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "lava.json"), []byte(`{"name":"from-the-user-directory","version":1,"zones":{}}`), 0o644); err != nil { t.Fatal(err) } t.Chdir(here) path, err := LoadProfile("profiles/lava.json") if err != nil { t.Fatalf("LoadProfile(profiles/lava.json): %v", err) } if path.Name != "from-the-checkout" { t.Errorf("LoadProfile(profiles/lava.json).Name = %q, want the file it names", path.Name) } byName, err := LoadProfile("lava") if err != nil { t.Fatalf("LoadProfile(lava): %v", err) } if byName.Name != "from-the-user-directory" { t.Errorf("LoadProfile(lava).Name = %q, want the user's directory, not the checkout's", byName.Name) } } // A path that is not there says which path, and not the user's directory: it was // never looked for there, so naming it sends the user to read a file that is // present and say nothing about why it was ignored. func TestLoadProfileAMissingPathNamesThePath(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) t.Chdir(t.TempDir()) _, err := LoadProfile("./nope.json") if err == nil { t.Fatal("LoadProfile(./nope.json) = nil error, want one") } if !strings.Contains(err.Error(), "./nope.json") { t.Errorf("LoadProfile(./nope.json) = %q, want it to name the path", err) } if strings.Contains(err.Error(), "qmk-rgb-tool") { t.Errorf("LoadProfile(./nope.json) = %q, want it not to name the user's directory", err) } } // A .json argument is read as a path and never sanitized into a name. This is the // case the rule turns on: `lala.json` used to mean the profile `lala-json`, and // the error for a missing one would name a file that never existed. func TestLoadProfileAJsonArgumentIsNotSanitized(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) t.Chdir(t.TempDir()) _, err := LoadProfile("lala.json") if err == nil { t.Fatal("LoadProfile(lala.json) = nil error, want one") } if strings.Contains(err.Error(), "lala-json") { t.Errorf("LoadProfile(lala.json) = %q, want lala.json read as a path", err) } if !strings.Contains(err.Error(), "lala.json") { t.Errorf("LoadProfile(lala.json) = %q, want it to name the path it tried", err) } } // A save that names a file writes that file, and the profile's own name is the // file's name without the suffix — so the file and the name in it agree, and the // file is not silently also written where the names live. func TestSaveProfileToWritesThePathItWasGiven(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) here := t.TempDir() t.Chdir(here) path, name, isFile := resolveProfileTarget("profiles/new.json") if !isFile { t.Fatal(`resolveProfileTarget("profiles/new.json") isPath = false, want true`) } p := &Profile{Name: name, Version: 1} if err := p.saveTo(path); err != nil { t.Fatalf("saveTo(%q): %v", path, err) } written := filepath.Join(here, "profiles", "new.json") if _, err := os.Stat(written); err != nil { t.Fatalf("saveTo(%q) did not write the file it was given: %v", path, err) } data, err := os.ReadFile(written) if err != nil { t.Fatal(err) } var got Profile if err := json.Unmarshal(data, &got); err != nil { t.Fatal(err) } if got.Name != "new" { t.Errorf("saved profile Name = %q, want %q", got.Name, "new") } if _, err := os.Stat(filepath.Join(userDir, "qmk-rgb-tool", "profiles", "new.json")); err == nil { t.Error("saveTo also wrote into the user's directory, which the argument did not ask for") } } // A save says which file it wrote, and the path is annotated the way every other // message in the tool is: a bare path does not say whether it is the user's // directory or one the argument named. func TestSavedProfileLineNamesTheFileItWrote(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) here := t.TempDir() t.Chdir(here) path, name, _ := resolveProfileTarget("profiles/new.json") got := savedProfileLine(name, path) if !strings.Contains(got, "profiles/new.json") { t.Errorf("savedProfileLine() = %q, want the path it was given", got) } if strings.Contains(got, "your user directory") { t.Errorf("savedProfileLine() = %q, want a file the argument named left unannotated", got) } byName, byNameStr, _ := resolveProfileTarget("lava") if line := savedProfileLine(byNameStr, byName); !strings.Contains(line, "your user directory") { t.Errorf("savedProfileLine() = %q, want a name's file marked as the user's", line) } } // A save by name goes to the user directory, which is the point of the name form: // the argument is a name, so it is the directory that decides where it goes. func TestSaveByNameGoesToTheUserDirectory(t *testing.T) { userDir := t.TempDir() t.Cleanup(stubUserConfigDir(t, userDir)) t.Cleanup(func() { profilesDirOverride = "" }) here := t.TempDir() t.Chdir(here) path, name, isFile := resolveProfileTarget("lava") if isFile { t.Fatal(`resolveProfileTarget("lava") isPath = true, want false`) } if err := (&Profile{Name: name, Version: 1}).saveTo(path); err != nil { t.Fatalf("saveTo(%q): %v", path, err) } if _, err := os.Stat(filepath.Join(userDir, "qmk-rgb-tool", "profiles", "lava.json")); err != nil { t.Fatalf("saveTo(%q) did not write into the user's directory: %v", path, err) } if _, err := os.Stat(filepath.Join(here, "lava.json")); err == nil { t.Error("saveTo wrote into the working directory, which a name does not ask for") } }