goyave.dev/goyave/v5@v5.0.0-rc9.0.20240517145003-d3f977d0b9f3/util/fsutil/fsutil_test.go (about) 1 package fsutil 2 3 import ( 4 "bytes" 5 "embed" 6 "encoding/json" 7 "io" 8 "io/fs" 9 "math" 10 "mime/multipart" 11 "net/textproto" 12 "os" 13 "path" 14 "path/filepath" 15 "runtime" 16 "strings" 17 "testing" 18 "time" 19 20 _ "embed" 21 22 "github.com/stretchr/testify/assert" 23 "github.com/stretchr/testify/require" 24 "goyave.dev/goyave/v5/util/errors" 25 "goyave.dev/goyave/v5/util/fsutil/osfs" 26 "goyave.dev/goyave/v5/util/typeutil" 27 ) 28 29 func deleteFile(path string) { 30 if err := os.Remove(path); err != nil { 31 panic(err) 32 } 33 } 34 35 func addFileToRequest(writer *multipart.Writer, path, name, fileName string) { 36 file, err := os.Open(path) 37 if err != nil { 38 panic(err) 39 } 40 defer func() { 41 _ = file.Close() 42 }() 43 part, err := writer.CreateFormFile(name, fileName) 44 if err != nil { 45 panic(err) 46 } 47 _, err = io.Copy(part, file) 48 if err != nil { 49 panic(err) 50 } 51 } 52 53 func createTestForm(files ...string) *multipart.Form { 54 _, filename, _, _ := runtime.Caller(1) 55 56 body := &bytes.Buffer{} 57 writer := multipart.NewWriter(body) 58 for _, p := range files { 59 fp := path.Dir(filename) + "/../../" + p 60 addFileToRequest(writer, fp, "file", filepath.Base(fp)) 61 } 62 err := writer.Close() 63 if err != nil { 64 panic(err) 65 } 66 67 reader := multipart.NewReader(body, writer.Boundary()) 68 form, err := reader.ReadForm(math.MaxInt64 - 1) 69 if err != nil { 70 panic(err) 71 } 72 return form 73 } 74 75 func createTestFiles(files ...string) []File { 76 form := createTestForm(files...) 77 f, err := ParseMultipartFiles(form.File["file"]) 78 if err != nil { 79 panic(err) 80 } 81 return f 82 } 83 84 func toAbsolutePath(relativePath string) string { 85 _, filename, _, _ := runtime.Caller(1) 86 return path.Dir(filename) + "/../../" + relativePath 87 } 88 89 func TestGetFileExtension(t *testing.T) { 90 assert.Equal(t, "png", GetFileExtension("test.png")) 91 assert.Equal(t, "gz", GetFileExtension("test.tar.gz")) 92 assert.Equal(t, "", GetFileExtension("test")) 93 } 94 95 func TestGetMIMEType(t *testing.T) { 96 mime, size, err := GetMIMEType(&osfs.FS{}, toAbsolutePath("resources/img/logo/goyave_16.png")) 97 assert.Equal(t, "image/png", mime) 98 assert.Equal(t, int64(716), size) 99 require.NoError(t, err) 100 101 mime, _, err = GetMIMEType(&osfs.FS{}, toAbsolutePath("resources/test_script.sh")) 102 require.NoError(t, err) 103 assert.Equal(t, "text/plain; charset=utf-8", mime) 104 105 mime, _, err = GetMIMEType(&osfs.FS{}, toAbsolutePath(".gitignore")) 106 require.NoError(t, err) 107 assert.Equal(t, "application/octet-stream", mime) 108 109 mime, _, err = GetMIMEType(&osfs.FS{}, toAbsolutePath("config/config.test.json")) 110 require.NoError(t, err) 111 assert.Equal(t, "application/json", mime) 112 113 mime, _, err = GetMIMEType(&osfs.FS{}, toAbsolutePath("resources/test_script.js")) 114 require.NoError(t, err) 115 assert.Equal(t, "text/javascript; charset=utf-8", mime) 116 117 cssPath := toAbsolutePath("util/fsutil/test.css") 118 err = os.WriteFile(cssPath, []byte("body{ margin:0; }"), 0644) 119 require.NoError(t, err) 120 mime, _, err = GetMIMEType(&osfs.FS{}, cssPath) 121 assert.Equal(t, "text/css", mime) 122 require.NoError(t, err) 123 deleteFile(cssPath) 124 125 _, _, err = GetMIMEType(&osfs.FS{}, toAbsolutePath("doesn't exist")) 126 require.Error(t, err) 127 128 t.Run("empty_file", func(t *testing.T) { 129 filename := "empty_GetMIMEType.json" 130 if err := os.WriteFile(filename, []byte{}, 0644); err != nil { 131 panic(err) 132 } 133 134 t.Cleanup(func() { 135 deleteFile(filename) 136 }) 137 138 mime, size, err = GetMIMEType(&osfs.FS{}, filename) 139 140 assert.Equal(t, "application/json", mime) 141 assert.Equal(t, int64(0), size) 142 require.NoError(t, err) 143 }) 144 } 145 146 func TestFileExists(t *testing.T) { 147 assert.True(t, FileExists(&osfs.FS{}, toAbsolutePath("resources/img/logo/goyave_16.png"))) 148 assert.False(t, FileExists(&osfs.FS{}, toAbsolutePath("doesn't exist"))) 149 } 150 151 func TestIsDirectory(t *testing.T) { 152 assert.True(t, IsDirectory(&osfs.FS{}, toAbsolutePath("resources/img/logo"))) 153 assert.False(t, IsDirectory(&osfs.FS{}, toAbsolutePath("resources/img/logo/goyave_16.png"))) 154 assert.False(t, IsDirectory(&osfs.FS{}, toAbsolutePath("doesn't exist"))) 155 } 156 157 func TestSave(t *testing.T) { 158 fs := &osfs.FS{} 159 file := createTestFiles("resources/img/logo/goyave_16.png")[0] 160 actualName, err := file.Save(fs, toAbsolutePath("."), "saved.png") 161 actualPath := toAbsolutePath(actualName) 162 assert.True(t, FileExists(fs, actualPath)) 163 assert.NoError(t, err) 164 165 deleteFile(actualPath) 166 assert.False(t, FileExists(fs, actualPath)) 167 168 file = createTestFiles("resources/img/logo/goyave_16.png")[0] 169 actualName, err = file.Save(fs, toAbsolutePath("."), "saved") 170 actualPath = toAbsolutePath(actualName) 171 assert.Equal(t, -1, strings.Index(actualName, ".")) 172 assert.True(t, FileExists(fs, actualPath)) 173 assert.NoError(t, err) 174 175 deleteFile(actualPath) 176 assert.False(t, FileExists(fs, actualPath)) 177 178 assert.Panics(t, func() { 179 deleteFile(actualPath) 180 }) 181 182 file = createTestFiles("resources/img/logo/goyave_16.png")[0] 183 path := toAbsolutePath("./subdir") 184 actualName, err = file.Save(fs, path, "saved") 185 actualPath = toAbsolutePath("./subdir/" + actualName) 186 assert.True(t, FileExists(fs, actualPath)) 187 assert.NoError(t, err) 188 189 assert.NoError(t, os.RemoveAll(path)) 190 assert.False(t, FileExists(fs, actualPath)) 191 192 file = createTestFiles("resources/img/logo/goyave_16.png")[0] 193 _, err = file.Save(fs, toAbsolutePath("./go.mod"), "saved") 194 assert.Error(t, err) 195 } 196 197 func TestMarshalFile(t *testing.T) { 198 type testDTO struct { 199 Files []File `json:"files"` 200 } 201 202 t.Run("success", func(t *testing.T) { 203 files := createTestFiles("resources/img/logo/goyave_16.png") 204 data := map[string]any{"files": files} 205 206 dto, err := typeutil.Convert[*testDTO](data) 207 require.NoError(t, err) 208 209 assert.Equal(t, files, dto.Files) 210 for i, f := range files { 211 assert.Same(t, f.Header, dto.Files[i].Header) 212 } 213 214 // Cache should be emptied. 215 cacheMu.RLock() 216 assert.Empty(t, marshalCache) 217 cacheMu.RUnlock() 218 }) 219 220 t.Run("unmarshal_err", func(t *testing.T) { 221 data := map[string]any{"files": 123} 222 223 _, err := typeutil.Convert[*testDTO](data) 224 require.Error(t, err) 225 assert.Contains(t, err.Error(), "cannot unmarshal number into Go struct field testDTO.files of type []fsutil.File") 226 }) 227 228 t.Run("unmarshal_nocache", func(t *testing.T) { 229 err := json.Unmarshal([]byte(`{"files": [{"Header":"uuid"}]}`), &testDTO{}) 230 require.Error(t, err) 231 assert.Contains(t, err.Error(), "cannot unmarshal fsutil.File: multipart header not found in cache") 232 }) 233 } 234 235 func TestOpenFileError(t *testing.T) { 236 dir := "./forbidden_directory" 237 assert.NoError(t, os.Mkdir(dir, 0500)) 238 defer func() { 239 assert.NoError(t, os.RemoveAll(dir)) 240 }() 241 file := createTestFiles("resources/img/logo/goyave_16.png")[0] 242 filename, err := file.Save(&osfs.FS{}, dir, "saved.png") 243 assert.Error(t, err) 244 assert.NotEmpty(t, filename) 245 } 246 247 func TestParseMultipartFiles(t *testing.T) { 248 249 t.Run("png", func(t *testing.T) { 250 form := createTestForm("resources/img/logo/goyave_16.png") 251 files, err := ParseMultipartFiles(form.File["file"]) 252 253 expected := []File{ 254 { 255 Header: form.File["file"][0], 256 MIMEType: "image/png", 257 }, 258 } 259 assert.Equal(t, expected, files) 260 assert.NoError(t, err) 261 }) 262 263 t.Run("empty_file", func(t *testing.T) { 264 headers := []*multipart.FileHeader{ 265 { 266 Filename: "empty_ParseMultipartFiles.json", 267 Size: 0, 268 Header: textproto.MIMEHeader{}, 269 }, 270 } 271 files, err := ParseMultipartFiles(headers) 272 273 expected := []File{ 274 { 275 Header: headers[0], 276 MIMEType: "application/octet-stream", 277 }, 278 } 279 assert.Equal(t, expected, files) 280 assert.NoError(t, err) 281 }) 282 } 283 284 //go:embed osfs 285 var resources embed.FS 286 287 type testStatFS struct { 288 embed.FS 289 } 290 291 type mockFileInfo struct{} 292 293 func (fs *mockFileInfo) Name() string { return "" } 294 func (fs *mockFileInfo) Size() int64 { return 0 } 295 func (fs *mockFileInfo) Mode() fs.FileMode { return 0 } 296 func (fs *mockFileInfo) ModTime() time.Time { return time.Now() } 297 func (fs *mockFileInfo) Sys() any { return nil } 298 func (fs *mockFileInfo) IsDir() bool { return false } 299 300 func (t testStatFS) Stat(_ string) (fileinfo fs.FileInfo, err error) { 301 return &mockFileInfo{}, nil 302 } 303 304 type mockFile struct { 305 name string 306 } 307 308 func (f *mockFile) Stat() (fs.FileInfo, error) { return nil, nil } 309 func (f *mockFile) Read(_ []byte) (int, error) { return 0, nil } 310 func (f *mockFile) Close() error { return nil } 311 312 type mockDirEntry struct{} 313 314 func (f *mockDirEntry) Name() string { return "" } 315 func (f *mockDirEntry) IsDir() bool { return false } 316 func (f *mockDirEntry) Type() fs.FileMode { return 0 } 317 func (f *mockDirEntry) Info() (fs.FileInfo, error) { return &mockFileInfo{}, nil } 318 319 type mockFS struct{} 320 321 func (e mockFS) Open(name string) (fs.File, error) { 322 return &mockFile{ 323 name: name, 324 }, nil 325 } 326 327 func (e mockFS) ReadDir(_ string) ([]fs.DirEntry, error) { 328 return []fs.DirEntry{&mockDirEntry{}}, nil 329 } 330 331 func TestEmbed(t *testing.T) { 332 e := NewEmbed(resources) 333 334 stat, err := e.Stat("osfs/osfs.go") 335 require.NoError(t, err) 336 assert.False(t, stat.IsDir()) 337 assert.Equal(t, "osfs.go", stat.Name()) 338 339 stat, err = e.Stat("notadir/osfs.go") 340 assert.Nil(t, stat) 341 if assert.Error(t, err) { 342 e, ok := err.(*errors.Error) 343 if assert.True(t, ok) { 344 var fsErr *fs.PathError 345 if assert.ErrorAs(t, e, &fsErr) { 346 assert.Equal(t, "open", fsErr.Op) 347 assert.Equal(t, "notadir/osfs.go", fsErr.Path) 348 } 349 } 350 } 351 352 // Make it so the underlying FS implements 353 e.FS = testStatFS{resources} 354 stat, err = e.Stat("osfs/osfs.go") 355 require.NoError(t, err) 356 _, ok := stat.(*mockFileInfo) 357 assert.True(t, ok) 358 359 t.Run("Open", func(t *testing.T) { 360 e := NewEmbed(&mockFS{}) 361 362 f, err := e.Open("") 363 require.NoError(t, err) 364 _, ok := f.(*mockFile) 365 assert.True(t, ok) 366 }) 367 t.Run("ReadDir", func(t *testing.T) { 368 e := NewEmbed(&mockFS{}) 369 370 f, err := e.ReadDir("") 371 require.NoError(t, err) 372 require.Len(t, f, 1) 373 _, ok := f[0].(*mockDirEntry) 374 assert.True(t, ok) 375 }) 376 } 377 378 func TestEmbedSub(t *testing.T) { 379 t.Run("err", func(t *testing.T) { 380 e := NewEmbed(resources) 381 sub, err := e.Sub("..") 382 assert.Equal(t, Embed{}, sub) 383 assert.Error(t, err) 384 }) 385 386 t.Run("Valid", func(t *testing.T) { 387 e := NewEmbed(resources) 388 sub, err := e.Sub("osfs.go") // It is valid to do this 389 assert.NotNil(t, sub.FS) 390 assert.NoError(t, err) 391 }) 392 }