|
|
@@ -54,6 +54,179 @@ func TestFetchDefinitionStoresTheFileForTheBoard(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// A definition in the user directory may be one the user has edited, and nothing
|
|
|
+// on disk says which it is. So a second fetch must not replace it, and must say
|
|
|
+// that the file is there rather than write over it silently.
|
|
|
+func TestFetchDoesNotReplaceAStoredDefinition(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ t.Cleanup(definitionFlagRestore(t))
|
|
|
+ t.Cleanup(forceDefinitionsDir(t, dir))
|
|
|
+ t.Cleanup(forceFetchRestore(t))
|
|
|
+ stubPrepareTarget(t, 0x1234, 0x5678)
|
|
|
+
|
|
|
+ edited := `{"name":"Test Board","vendorProductId":305419896,"menus":[],"note":"hand edited"}`
|
|
|
+ if err := os.WriteFile(filepath.Join(dir, "test_board.json"), []byte(edited), 0o644); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // No HTTP stub: a request would panic here, which is the point. The user is
|
|
|
+ // told the file exists without waiting on the network.
|
|
|
+ cmd := NewKeyboardFetchCmd()
|
|
|
+ var out, errOut strings.Builder
|
|
|
+ cmd.SetOut(&out)
|
|
|
+ cmd.SetErr(&errOut)
|
|
|
+
|
|
|
+ err := cmd.Execute()
|
|
|
+ if err == nil {
|
|
|
+ t.Fatal("keyboard fetch = nil error, want a refusal to replace a stored definition")
|
|
|
+ }
|
|
|
+ if !strings.Contains(err.Error(), "already at") || !strings.Contains(err.Error(), "--force") {
|
|
|
+ t.Errorf("error = %q, want it to name the file and --force", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ got, readErr := os.ReadFile(filepath.Join(dir, "test_board.json"))
|
|
|
+ if readErr != nil {
|
|
|
+ t.Fatal(readErr)
|
|
|
+ }
|
|
|
+ if string(got) != edited {
|
|
|
+ t.Errorf("stored file = %q, want it untouched (%q)", got, edited)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// --force is the only way to replace a stored definition, and it has to say that
|
|
|
+// is what happened, because the file it wrote over is gone.
|
|
|
+func TestFetchForceReplacesAStoredDefinition(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ t.Cleanup(definitionFlagRestore(t))
|
|
|
+ t.Cleanup(forceDefinitionsDir(t, dir))
|
|
|
+ t.Cleanup(forceFetchRestore(t))
|
|
|
+ stubPrepareTarget(t, 0x1234, 0x5678)
|
|
|
+
|
|
|
+ if err := os.WriteFile(filepath.Join(dir, "test_board.json"),
|
|
|
+ []byte(`{"name":"Test Board","vendorProductId":305419896,"menus":[],"note":"hand edited"}`), 0o644); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
|
|
|
+ stubHTTPGet(t, func(url string) ([]byte, int, error) {
|
|
|
+ if strings.Contains(url, "/v3/") {
|
|
|
+ return []byte(served), http.StatusOK, nil
|
|
|
+ }
|
|
|
+ return nil, http.StatusNotFound, errors.New("404")
|
|
|
+ })
|
|
|
+
|
|
|
+ cmd := NewKeyboardFetchCmd()
|
|
|
+ var out, errOut strings.Builder
|
|
|
+ cmd.SetOut(&out)
|
|
|
+ cmd.SetErr(&errOut)
|
|
|
+ cmd.SetArgs([]string{"--force"})
|
|
|
+
|
|
|
+ if err := cmd.Execute(); err != nil {
|
|
|
+ t.Fatalf("keyboard fetch --force error = %v (stderr %q)", err, errOut.String())
|
|
|
+ }
|
|
|
+ if !strings.Contains(out.String(), "Replaced definition") {
|
|
|
+ t.Errorf("stdout = %q, want it to say the definition was replaced", out.String())
|
|
|
+ }
|
|
|
+
|
|
|
+ entries, err := os.ReadDir(dir)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(entries) != 1 {
|
|
|
+ t.Fatalf("definitions dir = %v, want the one file replaced in place", entries)
|
|
|
+ }
|
|
|
+ got, readErr := os.ReadFile(filepath.Join(dir, entries[0].Name()))
|
|
|
+ if readErr != nil {
|
|
|
+ t.Fatal(readErr)
|
|
|
+ }
|
|
|
+ if string(got) != served {
|
|
|
+ t.Errorf("stored file = %q, want what the server served (%q)", got, served)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// A definition for a different board is not a reason to refuse: the fetch is for
|
|
|
+// this one, and that one is still worth having alongside.
|
|
|
+func TestFetchStoresAlongsideAnotherBoardsDefinition(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ t.Cleanup(definitionFlagRestore(t))
|
|
|
+ t.Cleanup(forceDefinitionsDir(t, dir))
|
|
|
+ t.Cleanup(forceFetchRestore(t))
|
|
|
+ stubPrepareTarget(t, 0x1234, 0x5678)
|
|
|
+
|
|
|
+ if err := os.WriteFile(filepath.Join(dir, "other.json"),
|
|
|
+ []byte(`{"name":"Other","vendorId":"0x1111","productId":"0x2222"}`), 0o644); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+
|
|
|
+ served := `{"name":"Test Board","vendorProductId":305419896,"menus":[]}`
|
|
|
+ stubHTTPGet(t, func(string) ([]byte, int, error) {
|
|
|
+ return []byte(served), http.StatusOK, nil
|
|
|
+ })
|
|
|
+
|
|
|
+ cmd := NewKeyboardFetchCmd()
|
|
|
+ var out, errOut strings.Builder
|
|
|
+ cmd.SetOut(&out)
|
|
|
+ cmd.SetErr(&errOut)
|
|
|
+
|
|
|
+ if err := cmd.Execute(); err != nil {
|
|
|
+ t.Fatalf("keyboard fetch error = %v (stderr %q)", err, errOut.String())
|
|
|
+ }
|
|
|
+
|
|
|
+ entries, err := os.ReadDir(dir)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(entries) != 2 {
|
|
|
+ t.Errorf("definitions dir = %v, want the other board's file kept alongside", entries)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// The vendor issues one vendor and product ID to two models, so a directory can
|
|
|
+// hold two files describing the same board. --force has to leave one, not two:
|
|
|
+// which of them is read would otherwise depend on the order the directory comes
|
|
|
+// back in.
|
|
|
+func TestFetchForceLeavesOneFileForABoardWithTwo(t *testing.T) {
|
|
|
+ dir := t.TempDir()
|
|
|
+ t.Cleanup(definitionFlagRestore(t))
|
|
|
+ t.Cleanup(forceDefinitionsDir(t, dir))
|
|
|
+ t.Cleanup(forceFetchRestore(t))
|
|
|
+ stubPrepareTarget(t, 0x320F, 0x5055)
|
|
|
+
|
|
|
+ for _, name := range []string{"first.json", "second.json"} {
|
|
|
+ body := `{"name":"Twin","vendorId":"0x320F","productId":"0x5055","menus":[]}`
|
|
|
+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ served := `{"name":"Twin","vendorProductId":839864405,"menus":[]}`
|
|
|
+ stubHTTPGet(t, func(string) ([]byte, int, error) {
|
|
|
+ return []byte(served), http.StatusOK, nil
|
|
|
+ })
|
|
|
+
|
|
|
+ cmd := NewKeyboardFetchCmd()
|
|
|
+ var out, errOut strings.Builder
|
|
|
+ cmd.SetOut(&out)
|
|
|
+ cmd.SetErr(&errOut)
|
|
|
+ cmd.SetArgs([]string{"--force"})
|
|
|
+
|
|
|
+ if err := cmd.Execute(); err != nil {
|
|
|
+ t.Fatalf("keyboard fetch --force error = %v (stderr %q)", err, errOut.String())
|
|
|
+ }
|
|
|
+
|
|
|
+ entries, err := os.ReadDir(dir)
|
|
|
+ if err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(entries) != 1 {
|
|
|
+ names := make([]string, 0, len(entries))
|
|
|
+ for _, e := range entries {
|
|
|
+ names = append(names, e.Name())
|
|
|
+ }
|
|
|
+ t.Errorf("definitions dir = %v, want exactly one file for the board", names)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// An unknown board is answered with a web page and a success status, so the
|
|
|
// fetch must not store it and must say what happened instead.
|
|
|
func TestFetchDefinitionRejectsAPageThatIsNotADefinition(t *testing.T) {
|
|
|
@@ -210,6 +383,14 @@ func definitionFlagRestore(t *testing.T) func() {
|
|
|
return func() { definitionFlag = original }
|
|
|
}
|
|
|
|
|
|
+// forceFetchRestore resets --force, so a test that sets it does not leak it into
|
|
|
+// the next one.
|
|
|
+func forceFetchRestore(t *testing.T) func() {
|
|
|
+ t.Helper()
|
|
|
+ original := forceFetch
|
|
|
+ return func() { forceFetch = original }
|
|
|
+}
|
|
|
+
|
|
|
// forceDefinitionsDir points the data directory at a test directory. The tool
|
|
|
// looks for it next to the executable and then in the working directory, so a
|
|
|
// test that wants its own has to make that lookup find it.
|