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  }