diff --git a/attachment/store_file_test.go b/attachment/store_file_test.go index 998a2bea..d0b6e135 100644 --- a/attachment/store_file_test.go +++ b/attachment/store_file_test.go @@ -1,180 +1,16 @@ package attachment import ( - "bytes" - "fmt" - "io" - "os" - "strings" "testing" - "time" "github.com/stretchr/testify/require" - "heckel.io/ntfy/v2/util" ) -var ( - oneKilobyteArray = make([]byte, 1024) -) - -func TestFileStore_Write_Success(t *testing.T) { - dir, c := newTestFileStore(t) - size, err := c.Write("abcdefghijkl", strings.NewReader("normal file"), 0, util.NewFixedLimiter(999)) - require.Nil(t, err) - require.Equal(t, int64(11), size) - require.Equal(t, "normal file", readFile(t, dir+"/abcdefghijkl")) - require.Equal(t, int64(11), c.Size()) - require.Equal(t, int64(10229), c.Remaining()) -} - -func TestFileStore_Write_Read_Success(t *testing.T) { - _, c := newTestFileStore(t) - size, err := c.Write("abcdefghijkl", strings.NewReader("hello world"), 0) - require.Nil(t, err) - require.Equal(t, int64(11), size) - - reader, readSize, err := c.Read("abcdefghijkl") - require.Nil(t, err) - require.Equal(t, int64(11), readSize) - defer reader.Close() - data, err := io.ReadAll(reader) - require.Nil(t, err) - require.Equal(t, "hello world", string(data)) -} - -func TestFileStore_Write_Remove_Success(t *testing.T) { - dir, c := newTestFileStore(t) // max = 10k (10240), each = 1k (1024) - for i := 0; i < 10; i++ { // 10x999 = 9990 - size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(make([]byte, 999)), 0) - require.Nil(t, err) - require.Equal(t, int64(999), size) - } - require.Equal(t, int64(9990), c.Size()) - require.Equal(t, int64(250), c.Remaining()) - require.FileExists(t, dir+"/abcdefghijk1") - require.FileExists(t, dir+"/abcdefghijk5") - - require.Nil(t, c.Remove("abcdefghijk1", "abcdefghijk5")) - require.NoFileExists(t, dir+"/abcdefghijk1") - require.NoFileExists(t, dir+"/abcdefghijk5") - require.Equal(t, int64(8*999), c.Size()) - require.Equal(t, int64(10240-8*999), c.Remaining()) -} - -func TestFileStore_Write_FailedTotalSizeLimit(t *testing.T) { - dir, c := newTestFileStore(t) - for i := 0; i < 10; i++ { - size, err := c.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(oneKilobyteArray), 0) - require.Nil(t, err) - require.Equal(t, int64(1024), size) - } - _, err := c.Write("abcdefghijkX", bytes.NewReader(oneKilobyteArray), 0) - require.Equal(t, util.ErrLimitReached, err) - require.NoFileExists(t, dir+"/abcdefghijkX") -} - -func TestFileStore_Write_FailedAdditionalLimiter(t *testing.T) { - dir, c := newTestFileStore(t) - _, err := c.Write("abcdefghijkl", bytes.NewReader(make([]byte, 1001)), 0, util.NewFixedLimiter(1000)) - require.Equal(t, util.ErrLimitReached, err) - require.NoFileExists(t, dir+"/abcdefghijkl") -} - -func TestFileStore_Write_UntrustedContentLengthExact(t *testing.T) { - dir, c := newTestFileStore(t) - size, err := c.Write("abcdefghijkl", strings.NewReader("hello world"), 11) - require.Nil(t, err) - require.Equal(t, int64(11), size) - require.Equal(t, "hello world", readFile(t, dir+"/abcdefghijkl")) -} - -func TestFileStore_Write_UntrustedContentLengthBodyLonger(t *testing.T) { - dir, c := newTestFileStore(t) - // Body has 11 bytes, but we claim 5 — only first 5 bytes should be stored - size, err := c.Write("abcdefghijkl", strings.NewReader("hello world"), 5) - require.Nil(t, err) - require.Equal(t, int64(5), size) - require.Equal(t, "hello", readFile(t, dir+"/abcdefghijkl")) -} - -func TestFileStore_Write_UntrustedContentLengthBodyShorter(t *testing.T) { - dir, c := newTestFileStore(t) - // Body has 5 bytes, but we claim 100 — should fail with content length mismatch - _, err := c.Write("abcdefghijkl", strings.NewReader("hello"), 100) - require.Error(t, err) - require.Contains(t, err.Error(), "content length mismatch") - require.NoFileExists(t, dir+"/abcdefghijkl") -} - -func TestFileStore_Read_NotFound(t *testing.T) { - _, c := newTestFileStore(t) - _, _, err := c.Read("abcdefghijkl") - require.Error(t, err) -} - -func TestFileStore_Sync(t *testing.T) { - dir, c := newTestFileStore(t) - - // Write some files - _, err := c.Write("abcdefghijk0", strings.NewReader("file0"), 0) - require.Nil(t, err) - _, err = c.Write("abcdefghijk1", strings.NewReader("file1"), 0) - require.Nil(t, err) - _, err = c.Write("abcdefghijk2", strings.NewReader("file2"), 0) - require.Nil(t, err) - - require.Equal(t, int64(15), c.Size()) - - // Set the ID provider to only know about file 0 and 2 - c.localIDs = func() ([]string, error) { - return []string{"abcdefghijk0", "abcdefghijk2"}, nil - } - - // Make file 1's mod time old enough to be cleaned up (> 1 hour) - oldTime := time.Unix(1, 0) - os.Chtimes(dir+"/abcdefghijk1", oldTime, oldTime) - - // Run sync - require.Nil(t, c.sync()) - - // File 1 should be deleted (orphan, old enough) - require.NoFileExists(t, dir+"/abcdefghijk1") - require.FileExists(t, dir+"/abcdefghijk0") - require.FileExists(t, dir+"/abcdefghijk2") - - // Size should be updated - require.Equal(t, int64(10), c.Size()) -} - -func TestFileStore_Sync_SkipsRecentFiles(t *testing.T) { - dir, c := newTestFileStore(t) - - // Write a file - _, err := c.Write("abcdefghijk0", strings.NewReader("file0"), 0) - require.Nil(t, err) - - // Set the ID provider to return empty (no valid IDs) - c.localIDs = func() ([]string, error) { - return []string{}, nil - } - - // File was just created, so it should NOT be deleted (< 1 hour old) - require.Nil(t, c.sync()) - require.FileExists(t, dir+"/abcdefghijk0") -} - -func newTestFileStore(t *testing.T) (dir string, cache *Store) { +func newTestFileStore(t *testing.T, totalSizeLimit int64) (dir string, cache *Store) { t.Helper() dir = t.TempDir() - cache, err := NewFileStore(dir, 10*1024, nil) + cache, err := NewFileStore(dir, totalSizeLimit, nil) require.Nil(t, err) t.Cleanup(func() { cache.Close() }) return dir, cache } - -func readFile(t *testing.T, f string) string { - t.Helper() - b, err := os.ReadFile(f) - require.Nil(t, err) - return string(b) -} diff --git a/attachment/store_s3_test.go b/attachment/store_s3_test.go index 37bd0ecb..2d4635ff 100644 --- a/attachment/store_s3_test.go +++ b/attachment/store_s3_test.go @@ -1,7 +1,6 @@ package attachment import ( - "bytes" "encoding/xml" "fmt" "io" @@ -14,43 +13,12 @@ import ( "github.com/stretchr/testify/require" "heckel.io/ntfy/v2/s3" - "heckel.io/ntfy/v2/util" ) -// --- Integration tests using a mock S3 server --- - -func TestS3Store_WriteReadRemove(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - // Write - size, err := cache.Write("abcdefghijkl", strings.NewReader("hello world"), 0) - require.Nil(t, err) - require.Equal(t, int64(11), size) - require.Equal(t, int64(11), cache.Size()) - - // Read back - reader, readSize, err := cache.Read("abcdefghijkl") - require.Nil(t, err) - require.Equal(t, int64(11), readSize) - data, err := io.ReadAll(reader) - reader.Close() - require.Nil(t, err) - require.Equal(t, "hello world", string(data)) - - // Remove - require.Nil(t, cache.Remove("abcdefghijkl")) - require.Equal(t, int64(0), cache.Size()) - - // Read after remove should fail - _, _, err = cache.Read("abcdefghijkl") - require.Error(t, err) -} +// --- S3-specific tests --- func TestS3Store_WriteNoPrefix(t *testing.T) { - server := newMockS3Server() + server, _ := newMockS3Server() defer server.Close() cache := newTestS3Store(t, server, "my-bucket", "", 10*1024) @@ -67,131 +35,6 @@ func TestS3Store_WriteNoPrefix(t *testing.T) { require.Equal(t, "test", string(data)) } -func TestS3Store_WriteTotalSizeLimit(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 100) - - // First write fits - _, err := cache.Write("abcdefghijk0", bytes.NewReader(make([]byte, 80)), 0) - require.Nil(t, err) - require.Equal(t, int64(80), cache.Size()) - require.Equal(t, int64(20), cache.Remaining()) - - // Second write exceeds total limit - _, err = cache.Write("abcdefghijk1", bytes.NewReader(make([]byte, 50)), 0) - require.ErrorIs(t, err, util.ErrLimitReached) -} - -func TestS3Store_WriteFileSizeLimit(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - _, err := cache.Write("abcdefghijkl", bytes.NewReader(make([]byte, 200)), 0, util.NewFixedLimiter(100)) - require.ErrorIs(t, err, util.ErrLimitReached) -} - -func TestS3Store_WriteRemoveMultiple(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - for i := 0; i < 5; i++ { - _, err := cache.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(make([]byte, 100)), 0) - require.Nil(t, err) - } - require.Equal(t, int64(500), cache.Size()) - - require.Nil(t, cache.Remove("abcdefghijk1", "abcdefghijk3")) - require.Equal(t, int64(300), cache.Size()) -} - -func TestS3Store_ReadNotFound(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - _, _, err := cache.Read("abcdefghijkl") - require.Error(t, err) -} - -func TestS3Store_InvalidID(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - _, err := cache.Write("bad", strings.NewReader("x"), 0) - require.Equal(t, errInvalidFileID, err) - - _, _, err = cache.Read("bad") - require.Equal(t, errInvalidFileID, err) - - err = cache.Remove("bad") - require.Equal(t, errInvalidFileID, err) -} - -func TestS3Store_Sync(t *testing.T) { - server := newMockS3Server() - defer server.Close() - - cache := newTestS3Store(t, server, "my-bucket", "pfx", 10*1024) - - // Write some files - _, err := cache.Write("abcdefghijk0", strings.NewReader("file0"), 0) - require.Nil(t, err) - _, err = cache.Write("abcdefghijk1", strings.NewReader("file1"), 0) - require.Nil(t, err) - _, err = cache.Write("abcdefghijk2", strings.NewReader("file2"), 0) - require.Nil(t, err) - - require.Equal(t, int64(15), cache.Size()) - - // Set the ID provider to only know about file 0 and 2 - // All mock objects have LastModified set to 2 hours ago, so orphans are eligible for deletion - cache.localIDs = func() ([]string, error) { - return []string{"abcdefghijk0", "abcdefghijk2"}, nil - } - - // Run sync - require.Nil(t, cache.sync()) - - // File 1 should be deleted (orphan) - _, _, err = cache.Read("abcdefghijk1") - require.Error(t, err) - - // Size should be updated - require.Equal(t, int64(10), cache.Size()) -} - -func TestS3Store_Sync_SkipsRecentFiles(t *testing.T) { - mockServer := newMockS3ServerWithModTime(time.Now()) - defer mockServer.Close() - - cache := newTestS3Store(t, mockServer, "my-bucket", "pfx", 10*1024) - - _, err := cache.Write("abcdefghijk0", strings.NewReader("file0"), 0) - require.Nil(t, err) - - // Set the ID provider to return empty (no valid IDs) - cache.localIDs = func() ([]string, error) { - return []string{}, nil - } - - // File was "just created" (mock returns recent time), so it should NOT be deleted - require.Nil(t, cache.sync()) - - // File should still exist - reader, _, err := cache.Read("abcdefghijk0") - require.Nil(t, err) - reader.Close() -} - // --- Helpers --- func newTestS3Store(t *testing.T, server *httptest.Server, bucket, prefix string, totalSizeLimit int64) *Store { @@ -219,24 +62,26 @@ func newTestS3Store(t *testing.T, server *httptest.Server, bucket, prefix string // ListObjectsV2. Uses path-style addressing: /{bucket}/{key}. Objects are stored in memory. type mockS3Server struct { - objects map[string][]byte // full key (bucket/key) -> body - uploads map[string]map[int][]byte // uploadID -> partNumber -> data - nextID int // counter for generating upload IDs - lastModTime time.Time // time to return for LastModified in list responses - mu sync.RWMutex + objects map[string][]byte // full key (bucket/key) -> body + modTimes map[string]time.Time // full key (bucket/key) -> last modified time + uploads map[string]map[int][]byte // uploadID -> partNumber -> data + nextID int // counter for generating upload IDs + mu sync.RWMutex } -func newMockS3Server() *httptest.Server { - return newMockS3ServerWithModTime(time.Now().Add(-2 * time.Hour)) -} - -func newMockS3ServerWithModTime(modTime time.Time) *httptest.Server { +func newMockS3Server() (*httptest.Server, *mockS3Server) { m := &mockS3Server{ - objects: make(map[string][]byte), - uploads: make(map[string]map[int][]byte), - lastModTime: modTime, + objects: make(map[string][]byte), + modTimes: make(map[string]time.Time), + uploads: make(map[string]map[int][]byte), } - return httptest.NewTLSServer(m) + return httptest.NewTLSServer(m), m +} + +func (m *mockS3Server) setModTime(path string, t time.Time) { + m.mu.Lock() + m.modTimes[path] = t + m.mu.Unlock() } func (m *mockS3Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -274,6 +119,7 @@ func (m *mockS3Server) handlePut(w http.ResponseWriter, r *http.Request, path st } m.mu.Lock() m.objects[path] = body + m.modTimes[path] = time.Now() m.mu.Unlock() w.WriteHeader(http.StatusOK) } @@ -333,6 +179,7 @@ func (m *mockS3Server) handleCompleteMultipart(w http.ResponseWriter, r *http.Re assembled = append(assembled, parts[i]...) } m.objects[path] = assembled + m.modTimes[path] = time.Now() delete(m.uploads, uploadID) m.mu.Unlock() @@ -402,7 +249,7 @@ func (m *mockS3Server) handleList(w http.ResponseWriter, r *http.Request, bucket contents = append(contents, s3ListObject{ Key: objKey, Size: int64(len(body)), - LastModified: m.lastModTime.Format(time.RFC3339), + LastModified: m.modTimes[key].Format(time.RFC3339), }) } } diff --git a/attachment/store_test.go b/attachment/store_test.go new file mode 100644 index 00000000..7b5a6013 --- /dev/null +++ b/attachment/store_test.go @@ -0,0 +1,252 @@ +package attachment + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "heckel.io/ntfy/v2/util" +) + +const testSizeLimit = 10 * 1024 + +func TestStore_WriteReadRemove(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + // Write + size, err := s.Write("abcdefghijkl", strings.NewReader("hello world"), 0) + require.Nil(t, err) + require.Equal(t, int64(11), size) + require.Equal(t, int64(11), s.Size()) + + // Read back + reader, readSize, err := s.Read("abcdefghijkl") + require.Nil(t, err) + require.Equal(t, int64(11), readSize) + data, err := io.ReadAll(reader) + reader.Close() + require.Nil(t, err) + require.Equal(t, "hello world", string(data)) + + // Remove + require.Nil(t, s.Remove("abcdefghijkl")) + require.Equal(t, int64(0), s.Size()) + + // Read after remove should fail + _, _, err = s.Read("abcdefghijkl") + require.Error(t, err) + }) +} + +func TestStore_WriteRemoveMultiple(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + for i := 0; i < 5; i++ { + _, err := s.Write(fmt.Sprintf("abcdefghijk%d", i), bytes.NewReader(make([]byte, 100)), 0) + require.Nil(t, err) + } + require.Equal(t, int64(500), s.Size()) + + require.Nil(t, s.Remove("abcdefghijk1", "abcdefghijk3")) + require.Equal(t, int64(300), s.Size()) + + // Removed files should not be readable + _, _, err := s.Read("abcdefghijk1") + require.Error(t, err) + _, _, err = s.Read("abcdefghijk3") + require.Error(t, err) + + // Remaining files should still be readable + for _, id := range []string{"abcdefghijk0", "abcdefghijk2", "abcdefghijk4"} { + reader, _, err := s.Read(id) + require.Nil(t, err) + reader.Close() + } + }) +} + +func TestStore_WriteTotalSizeLimit(t *testing.T) { + forEachBackend(t, 100, func(t *testing.T, s *Store, _ func(string)) { + // First write fits + _, err := s.Write("abcdefghijk0", bytes.NewReader(make([]byte, 80)), 0) + require.Nil(t, err) + require.Equal(t, int64(80), s.Size()) + require.Equal(t, int64(20), s.Remaining()) + + // Second write exceeds total limit + _, err = s.Write("abcdefghijk1", bytes.NewReader(make([]byte, 50)), 0) + require.ErrorIs(t, err, util.ErrLimitReached) + }) +} + +func TestStore_WriteAdditionalLimiter(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + _, err := s.Write("abcdefghijkl", bytes.NewReader(make([]byte, 200)), 0, util.NewFixedLimiter(100)) + require.ErrorIs(t, err, util.ErrLimitReached) + + // File should not be readable (was cleaned up) + _, _, err = s.Read("abcdefghijkl") + require.Error(t, err) + }) +} + +func TestStore_WriteWithLimiter(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + size, err := s.Write("abcdefghijkl", strings.NewReader("normal file"), 0, util.NewFixedLimiter(999)) + require.Nil(t, err) + require.Equal(t, int64(11), size) + require.Equal(t, int64(11), s.Size()) + }) +} + +func TestStore_ReadNotFound(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + _, _, err := s.Read("abcdefghijkl") + require.Error(t, err) + }) +} + +func TestStore_InvalidID(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + _, err := s.Write("bad", strings.NewReader("x"), 0) + require.Equal(t, errInvalidFileID, err) + + _, _, err = s.Read("bad") + require.Equal(t, errInvalidFileID, err) + + err = s.Remove("bad") + require.Equal(t, errInvalidFileID, err) + }) +} + +func TestStore_WriteUntrustedLengthExact(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + size, err := s.Write("abcdefghijkl", strings.NewReader("hello world"), 11) + require.Nil(t, err) + require.Equal(t, int64(11), size) + + reader, _, err := s.Read("abcdefghijkl") + require.Nil(t, err) + data, err := io.ReadAll(reader) + reader.Close() + require.Nil(t, err) + require.Equal(t, "hello world", string(data)) + }) +} + +func TestStore_WriteUntrustedLengthBodyLonger(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + // Body has 11 bytes, but we claim 5 — only first 5 bytes should be stored + size, err := s.Write("abcdefghijkl", strings.NewReader("hello world"), 5) + require.Nil(t, err) + require.Equal(t, int64(5), size) + + reader, _, err := s.Read("abcdefghijkl") + require.Nil(t, err) + data, err := io.ReadAll(reader) + reader.Close() + require.Nil(t, err) + require.Equal(t, "hello", string(data)) + }) +} + +func TestStore_WriteUntrustedLengthBodyShorter(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + // Body has 5 bytes, but we claim 100 — should fail + _, err := s.Write("abcdefghijkl", strings.NewReader("hello"), 100) + require.Error(t, err) + + // File should not be readable (was cleaned up) + _, _, err = s.Read("abcdefghijkl") + require.Error(t, err) + }) +} + +func TestStore_Sync(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, makeOld func(string)) { + // Write some files + _, err := s.Write("abcdefghijk0", strings.NewReader("file0"), 0) + require.Nil(t, err) + _, err = s.Write("abcdefghijk1", strings.NewReader("file1"), 0) + require.Nil(t, err) + _, err = s.Write("abcdefghijk2", strings.NewReader("file2"), 0) + require.Nil(t, err) + + require.Equal(t, int64(15), s.Size()) + + // Set the ID provider to only know about file 0 and 2 + s.localIDs = func() ([]string, error) { + return []string{"abcdefghijk0", "abcdefghijk2"}, nil + } + + // Make file 1 old enough to be cleaned up + makeOld("abcdefghijk1") + + // Run sync + require.Nil(t, s.sync()) + + // File 1 should be deleted (orphan, old enough) + _, _, err = s.Read("abcdefghijk1") + require.Error(t, err) + + // Files 0 and 2 should still be readable + r, _, err := s.Read("abcdefghijk0") + require.Nil(t, err) + r.Close() + r, _, err = s.Read("abcdefghijk2") + require.Nil(t, err) + r.Close() + + // Size should be updated + require.Equal(t, int64(10), s.Size()) + }) +} + +func TestStore_Sync_SkipsRecentFiles(t *testing.T) { + forEachBackend(t, testSizeLimit, func(t *testing.T, s *Store, _ func(string)) { + // Write a file + _, err := s.Write("abcdefghijk0", strings.NewReader("file0"), 0) + require.Nil(t, err) + + // Set the ID provider to return empty (no valid IDs) + s.localIDs = func() ([]string, error) { + return []string{}, nil + } + + // File was just created, so it should NOT be deleted (< 1 hour old) + require.Nil(t, s.sync()) + + // File should still exist + reader, _, err := s.Read("abcdefghijk0") + require.Nil(t, err) + reader.Close() + }) +} + +// forEachBackend runs f against both the file and S3 backends. It also provides a makeOld +// callback that makes a specific object's timestamp old enough for orphan cleanup (> 1 hour). +// For the file backend, this uses os.Chtimes; for the S3 backend, it sets the object's +// LastModified time in the mock server. Objects start with recent timestamps by default. +func forEachBackend(t *testing.T, totalSizeLimit int64, f func(t *testing.T, s *Store, makeOld func(string))) { + t.Run("file", func(t *testing.T) { + dir, s := newTestFileStore(t, totalSizeLimit) + makeOld := func(id string) { + oldTime := time.Unix(1, 0) + os.Chtimes(filepath.Join(dir, id), oldTime, oldTime) + } + f(t, s, makeOld) + }) + t.Run("s3", func(t *testing.T) { + server, mock := newMockS3Server() + defer server.Close() + s := newTestS3Store(t, server, "my-bucket", "pfx", totalSizeLimit) + makeOld := func(id string) { + mock.setModTime("my-bucket/pfx/"+id, time.Unix(1, 0)) + } + f(t, s, makeOld) + }) +}