package main import ( "errors" "fmt" "io" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/require" ) const filecontent = "the content of my file" type errorReader struct{} func (errorReader) Read(_ []byte) (int, error) { return 0, errors.New("reading error") } var _ io.Reader = errorReader{} func nameGenAddress(f NameGenerator) string { return fmt.Sprintf("%p", f) } func initStore(t *testing.T, path string) *Filestore { t.Helper() fs, err := NewFilestore(path) require.NoError(t, err, "cannot initialize filestore") return fs } func TestFilestore(t *testing.T) { t.Parallel() const bucket = "foo" t.Run("Initialize a store", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) assert.NotNil(fs) }) t.Run("It should create its directories if they do not exist", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(filepath.Join(tmpDir, "foo", "bar", "baz")) assert.NoError(err) assert.NotNil(fs) }) t.Run("Initialize a store in a directory that can't be created", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() tmpDir = filepath.Join(tmpDir, "unwritable") err := os.Mkdir(tmpDir, 0o000) assert.NoError(err) tmpDir = filepath.Join(tmpDir, "subdir") os.WriteFile(tmpDir, []byte("foo"), 0o644) fs, err := NewFilestore(tmpDir) assert.Error(err) assert.Nil(fs) }) t.Run("Selecting a new bucket", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) b := fs.Bucket(bucket) assert.NotNil(b) }) t.Run("Select again a bucket", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) b := fs.Bucket(bucket) assert.NotNil(b) b2 := fs.Bucket(bucket) assert.NotNil(b2) assert.Same(b, b2) }) t.Run("The Default name generator is identity", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) b := fs.Bucket(bucket) assert.NotNil(b) assert.Equal(nameGenAddress(NameIdentity), nameGenAddress(b.nameGenerator)) }) t.Run("A custom name generator can be used", func(t *testing.T) { t.Parallel() nameGen := func(s string) (string, error) { return s, nil } assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir, WithNameGen(nameGen)) assert.NoError(err) b := fs.Bucket(bucket) assert.NotNil(b) assert.Equal(nameGenAddress(nameGen), nameGenAddress(b.nameGenerator)) }) } func TestBucket(t *testing.T) { t.Parallel() const bucket = "foo" t.Run("the name generator of a bucket can be overloaded", func(t *testing.T) { t.Parallel() nameGen := func(s string) (string, error) { return s, nil } assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) b := fs.Bucket(bucket) assert.NotNil(b) b.SetNameGenerator(nameGen) assert.NotEqual(nameGenAddress(fs.nameGenerator), nameGenAddress(b.nameGenerator)) assert.Equal(nameGenAddress(nameGen), nameGenAddress(b.nameGenerator)) }) } func TestBucketPut(t *testing.T) { t.Parallel() const ( bucket = "foo" filename = "foo.bar" ) t.Run("It should store a file", func(t *testing.T) { t.Parallel() tmpDir := t.TempDir() fs := initStore(t, tmpDir) assert := require.New(t) b := fs.Bucket(bucket) fr := strings.NewReader(filecontent) name, err := b.Put(filename, fr) assert.NoError(err, "cannot store file") assert.Equal("foo.bar", name) assert.FileExists(filepath.Join(tmpDir, bucket, filename), "the file does not exist on disk") defer b.Delete(filename) assert.True(b.Exists(name), "the file does not exist in the store") }) t.Run("It returns an error if the name cannot be generated", func(t *testing.T) { t.Parallel() errNameGen := func(_ string) (string, error) { return "", errFilenameGeneration } tmpDir := t.TempDir() assert := require.New(t) fs, err := NewFilestore(tmpDir, WithNameGen(errNameGen)) assert.NoError(err) fr := strings.NewReader(filecontent) name, err := fs.Bucket(bucket).Put(filename, fr) assert.Error(err) assert.Equal("", name) }) t.Run("It returns an error if the bucket dir cannot be created", func(t *testing.T) { t.Parallel() tmpDir := t.TempDir() assert := require.New(t) fs, err := NewFilestore(tmpDir) assert.NoError(err) err = os.WriteFile(filepath.Join(tmpDir, bucket), []byte("foo"), 0o644) assert.NoError(err) err = os.Chmod(tmpDir, 0o400) assert.NoError(err) defer func() { err = os.Chmod(tmpDir, 0o700) assert.NoError(err) }() fr := strings.NewReader(filecontent) name, err := fs.Bucket(bucket).Put(filename, fr) assert.Error(err) assert.Equal("", name) }) t.Run("It returns an error if the file cannot be opened", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) bucketDir := filepath.Join(tmpDir, bucket) err = os.Mkdir(bucketDir, 0o700) assert.NoError(err) err = os.Mkdir(filepath.Join(bucketDir, filename), 0o700) assert.NoError(err) fr := strings.NewReader(filecontent) name, err := fs.Bucket(bucket).Put(filename, fr) assert.Error(err) assert.Equal("", name) }) t.Run("It returns an error if the file cannot be written", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs, err := NewFilestore(tmpDir) assert.NoError(err) fr := errorReader{} name, err := fs.Bucket(bucket).Put(filename, fr) assert.Error(err) assert.Equal("", name) }) } func TestBucketGet(t *testing.T) { t.Parallel() const ( bucket = "foo" filename = "foo.bar" ) t.Run("It should retrieve a file", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs := initStore(t, tmpDir) b := fs.Bucket(bucket) fullpath := filepath.Join(tmpDir, bucket, filename) err := os.Mkdir(filepath.Dir(fullpath), 0o700) assert.NoError(err) err = os.WriteFile(fullpath, []byte(filecontent), 0o600) assert.NoError(err) defer b.Delete(filename) r, err := b.Get(filename) assert.NoError(err) defer func() { _ = r.Close() }() content, err := io.ReadAll(r) assert.NoError(err) assert.Equal(filecontent, string(content)) }) t.Run("It returns an error if the file is not found", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs := initStore(t, tmpDir) b := fs.Bucket(bucket) r, err := b.Get(filename) assert.ErrorIs(err, errFileNotFound) assert.Nil(r) }) t.Run("It returns an error if the file cannot be read", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs := initStore(t, tmpDir) b := fs.Bucket(bucket) fullpath := filepath.Join(tmpDir, bucket, filename) err := os.Mkdir(filepath.Dir(fullpath), 0o700) assert.NoError(err) //revive:disable-next-line:add-constant // Only used here for tests err = os.WriteFile(fullpath, []byte(filecontent), 0o100) assert.NoError(err) defer b.Delete(filename) r, err := b.Get(filename) assert.Error(err) assert.Nil(r) }) } func TestBucketExists(t *testing.T) { t.Parallel() const ( bucket = "foo" filename = "foo.bar" ) t.Run("It returns true if the file exists", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs := initStore(t, tmpDir) b := fs.Bucket(bucket) fullpath := filepath.Join(tmpDir, bucket, filename) err := os.Mkdir(filepath.Dir(fullpath), 0o700) assert.NoError(err) err = os.WriteFile(fullpath, []byte(filecontent), 0o600) assert.NoError(err) defer b.Delete(filename) assert.True(b.Exists(filename)) }) t.Run("It returns false if the file does not exist", func(t *testing.T) { t.Parallel() assert := require.New(t) tmpDir := t.TempDir() fs := initStore(t, tmpDir) b := fs.Bucket(bucket) assert.False(b.Exists(filename)) }) } func TestBucketDelete(t *testing.T) { t.Parallel() const ( bucket = "foo" filename = "foo.bar" ) t.Run("Removing a file (happy path)", func(t *testing.T) { t.Parallel() tmpDir := t.TempDir() fs := initStore(t, tmpDir) assert := require.New(t) fr := strings.NewReader(filecontent) name, err := fs.Bucket(bucket).Put(filename, fr) assert.NoError(err) defer fs.Bucket(bucket).Delete(filename) err = fs.Bucket(bucket).Delete(name) assert.NoError(err) assert.NoFileExists(filepath.Join(tmpDir, bucket, filename)) assert.False(fs.Bucket("foo").Exists(name)) }) }