package api import ( "errors" "fmt" "io/fs" "net/http" "strings" "syscall" "testing" "git.fabledsword.com/bvandeusen/minstrel/internal/library" ) const removeTestPath = "/music/Moe Shop/WWW (2020)/01 - WWW.mp3" // removeFailure builds the error a delete service returns when the file would // not go, wrapped the way lidarrquarantine.DeleteFile and tracks.RemoveTrack // wrap it — the mapping has to see through that. func removeFailure(errno syscall.Errno) error { return fmt.Errorf("delete file: %w", &library.FileRemoveError{ Path: removeTestPath, UID: 1000, GID: 1000, Err: &fs.PathError{Op: "remove", Path: removeTestPath, Err: errno}, }) } func TestFileRemoveAPIError(t *testing.T) { cases := []struct { name string errno syscall.Errno wantStatus int wantCode string wantIn []string }{ { name: "read-only mount", errno: syscall.EROFS, wantStatus: http.StatusConflict, wantCode: "library_not_writable", wantIn: []string{"uid 1000, gid 1000", "/music/Moe Shop/WWW (2020)", "read-only file system", "Nothing was deleted"}, }, { name: "permission denied", errno: syscall.EACCES, wantStatus: http.StatusConflict, wantCode: "library_not_writable", wantIn: []string{"permission denied", "Nothing was deleted"}, }, { name: "operation not permitted", errno: syscall.EPERM, wantStatus: http.StatusConflict, wantCode: "library_not_writable", wantIn: []string{"operation not permitted"}, }, { name: "i/o error", errno: syscall.EIO, wantStatus: http.StatusInternalServerError, wantCode: "file_delete_failed", wantIn: []string{removeTestPath, "input/output error", "Nothing was deleted"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { apiErr, ok := fileRemoveAPIError(removeFailure(tc.errno)) if !ok { t.Fatal("a wrapped *library.FileRemoveError was not recognised") } if apiErr.Status != tc.wantStatus || apiErr.Code != tc.wantCode { t.Fatalf("got %d %s, want %d %s", apiErr.Status, apiErr.Code, tc.wantStatus, tc.wantCode) } for _, want := range tc.wantIn { if !strings.Contains(apiErr.Message, want) { t.Errorf("message %q lacks %q", apiErr.Message, want) } } }) } } // The unwritable answer must name the DIRECTORY. Removal needs write access to // the parent, so a message naming the file would send the operator to fix the // wrong permissions. The directory is a prefix of the file path, which is why a // plain "contains the directory" check could never catch that regression. func TestFileRemoveAPIError_NotWritableNamesTheDirectoryNotTheFile(t *testing.T) { apiErr, _ := fileRemoveAPIError(removeFailure(syscall.EROFS)) if strings.Contains(apiErr.Message, "01 - WWW.mp3") { t.Fatalf("message names the file rather than its directory: %q", apiErr.Message) } } func TestFileRemoveAPIError_IgnoresOtherErrors(t *testing.T) { for name, err := range map[string]error{ "nil": nil, "plain error": errors.New("delete track: connection reset"), "path error": &fs.PathError{Op: "remove", Path: removeTestPath, Err: syscall.EROFS}, "not found": library.ErrTrackNotFound, } { if _, ok := fileRemoveAPIError(err); ok { t.Errorf("%s: mapped as a file-remove failure", name) } } }