From de37cdaf0095c83df9b34da4e7ebb7bdcee9f6e5 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Tue, 22 Sep 2026 11:29:24 +0200 Subject: [PATCH 1/4] Add download for a device Download fetches the bundle the server holds for a device and writes it into a new or empty directory. It never reads the device. --- internal/files/files.go | 104 ++++++++++++++++++++++++++++++++ internal/files/files_test.go | 112 +++++++++++++++++++++++++++++++++++ main.go | 2 +- main_test.go | 2 +- 4 files changed, 218 insertions(+), 2 deletions(-) diff --git a/internal/files/files.go b/internal/files/files.go index 97e218f..2c6d243 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" @@ -182,3 +183,106 @@ func Upload(invocation api.Invocation, arguments []string) error { return nil } + +func Download(invocation api.Invocation, arguments []string) error { + if len(arguments) != 2 { + return errors.New("download takes an IMEI, then the directory to download into") + } + + imei := arguments[0] + isImei := len(imei) == 15 && !strings.ContainsFunc(imei, func(digit rune) bool { return digit < '0' || digit > '9' }) + + if !isImei { + return errors.New("download names one device by the 15-digit IMEI printed on it, not a fleet") + } + + destination := arguments[1] + info, err := os.Stat(destination) + + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("%s could not be read", destination) + } + + if err == nil && !info.IsDir() { + return fmt.Errorf("%s is a file, choose an empty or new directory", destination) + } + + if err == nil { + entries, err := os.ReadDir(destination) + + if err != nil { + return fmt.Errorf("%s could not be read", destination) + } + + if len(entries) > 0 { + return fmt.Errorf("%s is not empty, choose an empty or new directory", destination) + } + } + + request, err := api.AuthenticatedRequest(invocation, http.MethodGet, "/devices/"+imei+"/files", nil) + + if err != nil { + return err + } + + response, err := invocation.Client.Do(request) + + if err != nil { + return errors.New("the server could not be reached, check your internet access") + } + + defer response.Body.Close() + + if response.StatusCode != http.StatusOK { + return api.ServerError(response) + } + + result := struct { + Files map[string][]byte `json:"files"` + }{} + + err = api.Decode(response, &result) + + if err != nil { + return err + } + + paths := make([]string, 0, len(result.Files)) + + for path := range result.Files { + if path == "" || strings.HasPrefix(path, "/") || filepath.IsAbs(filepath.FromSlash(path)) || slices.Contains(strings.Split(path, "/"), "..") { + return errors.New("the download could not be trusted, so nothing was written") + } + + paths = append(paths, path) + } + + slices.Sort(paths) + + for _, path := range paths { + localPath := filepath.Join(destination, filepath.FromSlash(path)) + err = os.MkdirAll(filepath.Dir(localPath), 0o755) + + if err != nil { + return fmt.Errorf("%s could not be created", filepath.Dir(localPath)) + } + + err = os.WriteFile(localPath, result.Files[path], 0o644) + + if err != nil { + return fmt.Errorf("%s could not be written", localPath) + } + + fmt.Fprintln(invocation.Out, localPath) + } + + fileNoun := "files" + + if len(paths) == 1 { + fileNoun = "file" + } + + fmt.Fprintf(invocation.Out, "Downloaded %d %s from device %s into %s.\n", len(paths), fileNoun, imei, destination) + + return nil +} diff --git a/internal/files/files_test.go b/internal/files/files_test.go index a5bf41c..e38b863 100644 --- a/internal/files/files_test.go +++ b/internal/files/files_test.go @@ -173,3 +173,115 @@ func TestUploadArgumentsAndRefusal(t *testing.T) { t.Errorf("output = %q, want nothing", out.String()) } } + +func TestDownload(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("GET /devices/354820091234567/files", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"files":{"main.lua":"cHJpbnQoMSk=","lib/sensor.lua":"cmV0dXJuIDI="}}`)) + }) + mux.HandleFunc("GET /devices/354820099999999/files", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "no such device", http.StatusNotFound) + }) + mux.HandleFunc("GET /devices/354820098888888/files", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "no such device", http.StatusNotFound) + }) + mux.HandleFunc("GET /devices/354820097777777/files", func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "no code has been uploaded to this device", http.StatusNotFound) + }) + mux.HandleFunc("GET /devices/354820096666666/files", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"files":{"main.lua":"cHJpbnQoMSk=","../escape.lua":"cHJpbnQoMSk="}}`)) + }) + + occupied := writeTestProject(t) + file := filepath.Join(occupied, "main.lua") + + tests := []struct { + name string + imei string + destination string + wantFiles map[string]string + wantError string + }{ + { + name: "a device with code", + imei: "354820091234567", + destination: filepath.Join(t.TempDir(), "new"), + wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)"}, + }, + { + name: "an empty directory", + imei: "354820091234567", + destination: t.TempDir(), + wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)"}, + }, + {"an unknown device", "354820099999999", t.TempDir(), nil, "no such device"}, + {"a device in a fleet the user is not a member of", "354820098888888", t.TempDir(), nil, "no such device"}, + {"a device with no uploaded code", "354820097777777", t.TempDir(), nil, "no code has been uploaded to this device"}, + {"a fleet id as the target", "3", t.TempDir(), nil, "15-digit IMEI"}, + {"a wordy target", "rooftop", t.TempDir(), nil, "15-digit IMEI"}, + {"a non-empty directory", "354820091234567", occupied, nil, "choose an empty or new directory"}, + {"a file as the target", "354820091234567", file, nil, "choose an empty or new directory"}, + {"a bundle path that escapes the directory", "354820096666666", t.TempDir(), nil, "could not be trusted"}, + {"a missing argument", "354820091234567", "", nil, "takes an IMEI"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + invocation, out := apitest.LoggedInInvocation(t, mux) + arguments := []string{test.imei, test.destination} + + if test.destination == "" { + arguments = arguments[:1] + } + + err := Download(invocation, arguments) + + if test.wantError != "" { + if err == nil || !strings.Contains(err.Error(), test.wantError) { + t.Errorf("error = %v, want it to mention %q", err, test.wantError) + } + + if out.String() != "" { + t.Errorf("output = %q, want nothing", out.String()) + } + + entries, _ := os.ReadDir(test.destination) + + if test.destination != occupied && len(entries) > 0 { + t.Errorf("%d files were written, want none", len(entries)) + } + + return + } + + if err != nil { + t.Fatal(err) + } + + gotFiles := map[string]string{} + wantOutput := "" + + for _, path := range []string{"lib/sensor.lua", "main.lua"} { + localPath := filepath.Join(test.destination, filepath.FromSlash(path)) + content, err := os.ReadFile(localPath) + + if err != nil { + t.Fatal(err) + } + + gotFiles[path] = string(content) + wantOutput += localPath + "\n" + } + + wantOutput += "Downloaded 2 files from device " + test.imei + " into " + test.destination + ".\n" + + if !reflect.DeepEqual(gotFiles, test.wantFiles) { + t.Errorf("downloaded files = %v, want %v", gotFiles, test.wantFiles) + } + + if out.String() != wantOutput { + t.Errorf("output = %q, want %q", out.String(), wantOutput) + } + }) + } +} diff --git a/main.go b/main.go index 35e9104..fcef161 100644 --- a/main.go +++ b/main.go @@ -50,7 +50,7 @@ var sections = []dispatch.Section{ Title: "Files", Commands: []dispatch.Command{ {Name: "upload", Arguments: " ...", Summary: "Upload files or directories to a device or fleet", Run: files.Upload}, - {Name: "download", Arguments: " ", Summary: "Download a device or fleet's files into "}, + {Name: "download", Arguments: " ", Summary: "Download the code last uploaded to a device into ", Run: files.Download}, {Name: "dev", Arguments: " ... [--log-file ]", Summary: "Upload on every change, and tail"}, }, }, diff --git a/main_test.go b/main_test.go index da56442..9f8a8e4 100644 --- a/main_test.go +++ b/main_test.go @@ -46,7 +46,6 @@ func TestOnlyPlannedCommandsAreUnimplemented(t *testing.T) { "device start": true, "device stop": true, "device restart": true, - "download": true, "dev": true, "tail": true, } @@ -162,6 +161,7 @@ func TestTheTableWiresEveryCommandOffered(t *testing.T) { wired := []string{ "account balance", "account delete", "account topup", "device list", "device pair", "device rename", "device unpair", + "download", "fleet create", "fleet delete", "fleet list", "fleet rename", "fleet transfer", "key create", "key list", "key revoke", "login", "logout", From ef20710b93eccf4a1b34337656737b77ee3e40f5 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Tue, 22 Sep 2026 12:03:11 +0200 Subject: [PATCH 2/4] Ask before download replaces existing files Download accepted only an empty or new directory. It now writes into any directory, lists the bundle files that already exist there, and asks once before replacing them. --- internal/files/files.go | 43 +++++++---- internal/files/files_test.go | 144 ++++++++++++++++++++++++++++------- 2 files changed, 145 insertions(+), 42 deletions(-) diff --git a/internal/files/files.go b/internal/files/files.go index 2c6d243..debc0b1 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -1,6 +1,7 @@ package files import ( + "bufio" "bytes" "encoding/json" "errors" @@ -204,19 +205,7 @@ func Download(invocation api.Invocation, arguments []string) error { } if err == nil && !info.IsDir() { - return fmt.Errorf("%s is a file, choose an empty or new directory", destination) - } - - if err == nil { - entries, err := os.ReadDir(destination) - - if err != nil { - return fmt.Errorf("%s could not be read", destination) - } - - if len(entries) > 0 { - return fmt.Errorf("%s is not empty, choose an empty or new directory", destination) - } + return fmt.Errorf("%s is a file, choose a directory", destination) } request, err := api.AuthenticatedRequest(invocation, http.MethodGet, "/devices/"+imei+"/files", nil) @@ -259,6 +248,34 @@ func Download(invocation api.Invocation, arguments []string) error { slices.Sort(paths) + existing := []string{} + + for _, path := range paths { + localPath := filepath.Join(destination, filepath.FromSlash(path)) + _, err := os.Stat(localPath) + + if err == nil { + existing = append(existing, localPath) + } + } + + if len(existing) == 1 { + fmt.Fprintf(invocation.Out, "%s already exists. Replace it? [y/N] ", existing[0]) + } else if len(existing) > 1 { + fmt.Fprintf(invocation.Out, "These files already exist:\n%s\nReplace them? [y/N] ", strings.Join(existing, "\n")) + } + + if len(existing) > 0 { + answer, _ := bufio.NewReader(invocation.In).ReadString('\n') + + answer = strings.ToLower(strings.TrimSpace(answer)) + + if answer != "y" && answer != "yes" { + fmt.Fprintln(invocation.Out, "Nothing downloaded.") + return nil + } + } + for _, path := range paths { localPath := filepath.Join(destination, filepath.FromSlash(path)) err = os.MkdirAll(filepath.Dir(localPath), 0o755) diff --git a/internal/files/files_test.go b/internal/files/files_test.go index e38b863..7098641 100644 --- a/internal/files/files_test.go +++ b/internal/files/files_test.go @@ -2,6 +2,7 @@ package files import ( "encoding/json" + "io/fs" "net/http" "os" "path/filepath" @@ -192,42 +193,116 @@ func TestDownload(t *testing.T) { w.Write([]byte(`{"files":{"main.lua":"cHJpbnQoMSk=","../escape.lua":"cHJpbnQoMSk="}}`)) }) - occupied := writeTestProject(t) - file := filepath.Join(occupied, "main.lua") + directoryWith := func(files map[string]string) string { + directory := t.TempDir() + + for name, content := range files { + path := filepath.Join(directory, filepath.FromSlash(name)) + + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + + return directory + } + + downloaded := func(destination string) string { + return filepath.Join(destination, "lib", "sensor.lua") + "\n" + filepath.Join(destination, "main.lua") + "\n" + + "Downloaded 2 files from device 354820091234567 into " + destination + ".\n" + } + + bundle := map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)"} + fresh := filepath.Join(t.TempDir(), "new") + empty := t.TempDir() + unrelated := directoryWith(map[string]string{"notes.txt": "keep"}) + file := filepath.Join(unrelated, "notes.txt") + replaced := directoryWith(map[string]string{"main.lua": "print(0)"}) + declined := directoryWith(map[string]string{"main.lua": "print(0)"}) + unanswered := directoryWith(map[string]string{"main.lua": "print(0)"}) + both := directoryWith(map[string]string{"lib/sensor.lua": "return 0", "main.lua": "print(0)", "notes.txt": "keep"}) tests := []struct { name string imei string destination string + answer string wantFiles map[string]string + wantOutput string wantError string }{ { - name: "a device with code", + name: "a new directory", imei: "354820091234567", - destination: filepath.Join(t.TempDir(), "new"), - wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)"}, + destination: fresh, + wantFiles: bundle, + wantOutput: downloaded(fresh), }, { name: "an empty directory", imei: "354820091234567", - destination: t.TempDir(), - wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)"}, + destination: empty, + wantFiles: bundle, + wantOutput: downloaded(empty), + }, + { + name: "a directory with unrelated files", + imei: "354820091234567", + destination: unrelated, + wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)", "notes.txt": "keep"}, + wantOutput: downloaded(unrelated), + }, + { + name: "a conflicting file, replaced", + imei: "354820091234567", + destination: replaced, + answer: "y\n", + wantFiles: bundle, + wantOutput: filepath.Join(replaced, "main.lua") + " already exists. Replace it? [y/N] " + downloaded(replaced), + }, + { + name: "a conflicting file, declined", + imei: "354820091234567", + destination: declined, + answer: "n\n", + wantFiles: map[string]string{"main.lua": "print(0)"}, + wantOutput: filepath.Join(declined, "main.lua") + " already exists. Replace it? [y/N] Nothing downloaded.\n", + }, + { + name: "a conflicting file, answered with an empty line", + imei: "354820091234567", + destination: unanswered, + answer: "\n", + wantFiles: map[string]string{"main.lua": "print(0)"}, + wantOutput: filepath.Join(unanswered, "main.lua") + " already exists. Replace it? [y/N] Nothing downloaded.\n", }, - {"an unknown device", "354820099999999", t.TempDir(), nil, "no such device"}, - {"a device in a fleet the user is not a member of", "354820098888888", t.TempDir(), nil, "no such device"}, - {"a device with no uploaded code", "354820097777777", t.TempDir(), nil, "no code has been uploaded to this device"}, - {"a fleet id as the target", "3", t.TempDir(), nil, "15-digit IMEI"}, - {"a wordy target", "rooftop", t.TempDir(), nil, "15-digit IMEI"}, - {"a non-empty directory", "354820091234567", occupied, nil, "choose an empty or new directory"}, - {"a file as the target", "354820091234567", file, nil, "choose an empty or new directory"}, - {"a bundle path that escapes the directory", "354820096666666", t.TempDir(), nil, "could not be trusted"}, - {"a missing argument", "354820091234567", "", nil, "takes an IMEI"}, + { + name: "several conflicting files, replaced", + imei: "354820091234567", + destination: both, + answer: "yes\n", + wantFiles: map[string]string{"lib/sensor.lua": "return 2", "main.lua": "print(1)", "notes.txt": "keep"}, + wantOutput: "These files already exist:\n" + filepath.Join(both, "lib", "sensor.lua") + "\n" + filepath.Join(both, "main.lua") + + "\nReplace them? [y/N] " + downloaded(both), + }, + {"an unknown device", "354820099999999", t.TempDir(), "", nil, "", "no such device"}, + {"a device in a fleet the user is not a member of", "354820098888888", t.TempDir(), "", nil, "", "no such device"}, + {"a device with no uploaded code", "354820097777777", t.TempDir(), "", nil, "", "no code has been uploaded to this device"}, + {"a fleet id as the target", "3", t.TempDir(), "", nil, "", "15-digit IMEI"}, + {"a wordy target", "rooftop", t.TempDir(), "", nil, "", "15-digit IMEI"}, + {"a file as the target", "354820091234567", file, "", nil, "", "is a file, choose a directory"}, + {"a bundle path that escapes the directory", "354820096666666", t.TempDir(), "", nil, "", "could not be trusted"}, + {"a missing argument", "354820091234567", "", "", nil, "", "takes an IMEI"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { invocation, out := apitest.LoggedInInvocation(t, mux) + invocation.In = strings.NewReader(test.answer) arguments := []string{test.imei, test.destination} if test.destination == "" { @@ -247,7 +322,7 @@ func TestDownload(t *testing.T) { entries, _ := os.ReadDir(test.destination) - if test.destination != occupied && len(entries) > 0 { + if len(entries) > 0 { t.Errorf("%d files were written, want none", len(entries)) } @@ -259,28 +334,39 @@ func TestDownload(t *testing.T) { } gotFiles := map[string]string{} - wantOutput := "" - for _, path := range []string{"lib/sensor.lua", "main.lua"} { - localPath := filepath.Join(test.destination, filepath.FromSlash(path)) - content, err := os.ReadFile(localPath) + err = filepath.WalkDir(test.destination, func(path string, entry fs.DirEntry, walkError error) error { + if walkError != nil || entry.IsDir() { + return walkError + } + + content, err := os.ReadFile(path) if err != nil { - t.Fatal(err) + return err } - gotFiles[path] = string(content) - wantOutput += localPath + "\n" - } + relative, err := filepath.Rel(test.destination, path) - wantOutput += "Downloaded 2 files from device " + test.imei + " into " + test.destination + ".\n" + if err != nil { + return err + } + + gotFiles[filepath.ToSlash(relative)] = string(content) + + return nil + }) + + if err != nil { + t.Fatal(err) + } if !reflect.DeepEqual(gotFiles, test.wantFiles) { - t.Errorf("downloaded files = %v, want %v", gotFiles, test.wantFiles) + t.Errorf("files in %s = %v, want %v", test.destination, gotFiles, test.wantFiles) } - if out.String() != wantOutput { - t.Errorf("output = %q, want %q", out.String(), wantOutput) + if out.String() != test.wantOutput { + t.Errorf("output = %q, want %q", out.String(), test.wantOutput) } }) } From 0d98f5fb1a0ef643753dc4e6ac2d4b276303081b Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Tue, 22 Sep 2026 12:23:07 +0200 Subject: [PATCH 3/4] Separate error checks in the download test fixture The fixture combined each call with its error test, which the coding rules forbid. --- internal/files/files_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/files/files_test.go b/internal/files/files_test.go index 7098641..7f8d096 100644 --- a/internal/files/files_test.go +++ b/internal/files/files_test.go @@ -198,12 +198,15 @@ func TestDownload(t *testing.T) { for name, content := range files { path := filepath.Join(directory, filepath.FromSlash(name)) + err := os.MkdirAll(filepath.Dir(path), 0o755) - if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + if err != nil { t.Fatal(err) } - if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + err = os.WriteFile(path, []byte(content), 0o644) + + if err != nil { t.Fatal(err) } } From 868b9c49c2426791b6d812f2f3f30dd1da7bc336 Mon Sep 17 00:00:00 2001 From: Raj Nakarja Date: Thu, 24 Sep 2026 11:20:39 +0200 Subject: [PATCH 4/4] Refuse a download where a file blocks a bundle directory The existence check treated a path under such a file as absent, so no prompt was shown and every file before it in path order was written before the failure. Also use the singular when one file is uploaded. --- internal/files/files.go | 24 ++++++++++++++++++++---- internal/files/files_test.go | 26 +++++++++++++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/internal/files/files.go b/internal/files/files.go index debc0b1..8aa0206 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -143,9 +143,11 @@ func Upload(invocation api.Invocation, arguments []string) error { defer response.Body.Close() fileNoun := "files" + arrival := "They arrive" if len(collected) == 1 { fileNoun = "file" + arrival = "It arrives" } if isImei { @@ -153,8 +155,8 @@ func Upload(invocation api.Invocation, arguments []string) error { return api.ServerError(response) } - fmt.Fprintf(invocation.Out, "Uploaded %d %s to device %s. They arrive at its next check-in.\n", - len(collected), fileNoun, target) + fmt.Fprintf(invocation.Out, "Uploaded %d %s to device %s. %s at its next check-in.\n", + len(collected), fileNoun, target, arrival) return nil } @@ -179,8 +181,8 @@ func Upload(invocation api.Invocation, arguments []string) error { deviceNoun = "device" } - fmt.Fprintf(invocation.Out, "Uploaded %d %s to %d %s in fleet %d. They arrive at each device's next check-in.\n", - len(collected), fileNoun, result.Devices, deviceNoun, fleetID) + fmt.Fprintf(invocation.Out, "Uploaded %d %s to %d %s in fleet %d. %s at each device's next check-in.\n", + len(collected), fileNoun, result.Devices, deviceNoun, fleetID, arrival) return nil } @@ -248,6 +250,20 @@ func Download(invocation api.Invocation, arguments []string) error { slices.Sort(paths) + root := filepath.Clean(destination) + + for _, path := range paths { + localPath := filepath.Join(root, filepath.FromSlash(path)) + + for parent := filepath.Dir(localPath); parent != root; parent = filepath.Dir(parent) { + info, err := os.Stat(parent) + + if err == nil && !info.IsDir() { + return fmt.Errorf("%s is a file where the code needs a directory", parent) + } + } + } + existing := []string{} for _, path := range paths { diff --git a/internal/files/files_test.go b/internal/files/files_test.go index 7f8d096..28a4778 100644 --- a/internal/files/files_test.go +++ b/internal/files/files_test.go @@ -72,7 +72,7 @@ func TestUpload(t *testing.T) { arguments: []string{"3", filepath.Join(project, "main.lua")}, wantPath: "/fleets/3/files", wantFiles: map[string]string{"main.lua": "print(1)"}, - wantOutput: "Uploaded 1 file to 2 devices in fleet 3. They arrive at each device's next check-in.\n", + wantOutput: "Uploaded 1 file to 2 devices in fleet 3. It arrives at each device's next check-in.\n", }, } @@ -228,6 +228,7 @@ func TestDownload(t *testing.T) { declined := directoryWith(map[string]string{"main.lua": "print(0)"}) unanswered := directoryWith(map[string]string{"main.lua": "print(0)"}) both := directoryWith(map[string]string{"lib/sensor.lua": "return 0", "main.lua": "print(0)", "notes.txt": "keep"}) + blocked := directoryWith(map[string]string{"lib": "not a directory"}) tests := []struct { name string @@ -298,6 +299,13 @@ func TestDownload(t *testing.T) { {"a fleet id as the target", "3", t.TempDir(), "", nil, "", "15-digit IMEI"}, {"a wordy target", "rooftop", t.TempDir(), "", nil, "", "15-digit IMEI"}, {"a file as the target", "354820091234567", file, "", nil, "", "is a file, choose a directory"}, + { + name: "a file where the bundle has a directory", + imei: "354820091234567", + destination: blocked, + wantFiles: map[string]string{"lib": "not a directory"}, + wantError: filepath.Join(blocked, "lib") + " is a file where the code needs a directory", + }, {"a bundle path that escapes the directory", "354820096666666", t.TempDir(), "", nil, "", "could not be trusted"}, {"a missing argument", "354820091234567", "", "", nil, "", "takes an IMEI"}, } @@ -323,16 +331,16 @@ func TestDownload(t *testing.T) { t.Errorf("output = %q, want nothing", out.String()) } - entries, _ := os.ReadDir(test.destination) + if test.wantFiles == nil { + entries, _ := os.ReadDir(test.destination) - if len(entries) > 0 { - t.Errorf("%d files were written, want none", len(entries)) - } + if len(entries) > 0 { + t.Errorf("%d files were written, want none", len(entries)) + } - return - } - - if err != nil { + return + } + } else if err != nil { t.Fatal(err) }