aboutsummaryrefslogtreecommitdiff
path: root/hotline/mem_file_store_test.go
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-08 09:11:16 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-07-08 09:11:16 -0700
commit3e5ae31fa6d8c33f61d070ca20c34c0dcba7666b (patch)
tree248ec903bafebaaee3ddb4030ca70def39ac66ad /hotline/mem_file_store_test.go
parenta42a5961861f638fef3b49685fc6df2eea38fa0d (diff)
Add in-memory MemFileStore backend
Provide a reference non-OS FileStore that models storage as a flat keyspace of cleaned paths, deriving directory structure from key prefixes the way an object store does. It proves the FileStore abstraction is genuinely backend-agnostic and serves as a hermetic test fixture needing no temp directory. Symlink/ReadLink return errors.ErrUnsupported, matching how an object-store backend would behave. Tests drive a full upload -> list -> download round-trip and directory listing entirely in memory.
Diffstat (limited to 'hotline/mem_file_store_test.go')
-rw-r--r--hotline/mem_file_store_test.go166
1 files changed, 166 insertions, 0 deletions
diff --git a/hotline/mem_file_store_test.go b/hotline/mem_file_store_test.go
new file mode 100644
index 0000000..71fa339
--- /dev/null
+++ b/hotline/mem_file_store_test.go
@@ -0,0 +1,166 @@
+package hotline
+
+import (
+ "bytes"
+ "errors"
+ "io"
+ "io/fs"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/text/encoding"
+)
+
+func TestMemFileStore_WriteReadAppend(t *testing.T) {
+ m := NewMemFileStore()
+
+ // Create + write.
+ w, err := m.Create("/files/foo.txt")
+ require.NoError(t, err)
+ _, err = w.Write([]byte("hello"))
+ require.NoError(t, err)
+ require.NoError(t, w.Close())
+
+ got, err := m.ReadFile("/files/foo.txt")
+ require.NoError(t, err)
+ assert.Equal(t, []byte("hello"), got)
+
+ // Append via OpenFile with O_APPEND (mirrors the .incomplete upload path).
+ aw, err := m.OpenFile("/files/foo.txt", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+ require.NoError(t, err)
+ _, err = aw.Write([]byte(" world"))
+ require.NoError(t, err)
+ require.NoError(t, aw.Close())
+
+ got, err = m.ReadFile("/files/foo.txt")
+ require.NoError(t, err)
+ assert.Equal(t, []byte("hello world"), got)
+
+ // Open returns a reader over the committed bytes.
+ r, err := m.Open("/files/foo.txt")
+ require.NoError(t, err)
+ b, err := io.ReadAll(r)
+ require.NoError(t, err)
+ require.NoError(t, r.Close())
+ assert.Equal(t, []byte("hello world"), b)
+}
+
+func TestMemFileStore_StatAndParents(t *testing.T) {
+ m := NewMemFileStore()
+
+ require.NoError(t, m.WriteFile("/files/sub/bar.txt", []byte("data"), 0644))
+
+ // Nested write auto-registers parent directories.
+ fi, err := m.Stat("/files/sub")
+ require.NoError(t, err)
+ assert.True(t, fi.IsDir())
+
+ fi, err = m.Stat("/files/sub/bar.txt")
+ require.NoError(t, err)
+ assert.False(t, fi.IsDir())
+ assert.Equal(t, int64(4), fi.Size())
+
+ _, err = m.Stat("/files/missing")
+ assert.True(t, errors.Is(err, fs.ErrNotExist))
+}
+
+func TestMemFileStore_ReadDir(t *testing.T) {
+ m := NewMemFileStore()
+ require.NoError(t, m.WriteFile("/root/a.txt", []byte("a"), 0644))
+ require.NoError(t, m.WriteFile("/root/b.txt", []byte("bb"), 0644))
+ require.NoError(t, m.Mkdir("/root/sub", 0755))
+ require.NoError(t, m.WriteFile("/root/sub/deep.txt", []byte("x"), 0644))
+
+ entries, err := m.ReadDir("/root")
+ require.NoError(t, err)
+
+ var names []string
+ for _, e := range entries {
+ names = append(names, e.Name())
+ }
+ assert.Equal(t, []string{"a.txt", "b.txt", "sub"}, names)
+}
+
+func TestMemFileStore_RenameRemove(t *testing.T) {
+ m := NewMemFileStore()
+ require.NoError(t, m.WriteFile("/f/a.incomplete", []byte("payload"), 0644))
+
+ // Rename mirrors the incomplete->final upload commit.
+ require.NoError(t, m.Rename("/f/a.incomplete", "/f/a"))
+ _, err := m.Stat("/f/a.incomplete")
+ assert.True(t, errors.Is(err, fs.ErrNotExist))
+ got, err := m.ReadFile("/f/a")
+ require.NoError(t, err)
+ assert.Equal(t, []byte("payload"), got)
+
+ require.NoError(t, m.WriteFile("/f/sub/x", []byte("x"), 0644))
+ require.NoError(t, m.RemoveAll("/f/sub"))
+ _, err = m.Stat("/f/sub/x")
+ assert.True(t, errors.Is(err, fs.ErrNotExist))
+}
+
+func TestMemFileStore_WalkCounts(t *testing.T) {
+ m := NewMemFileStore()
+ require.NoError(t, m.Mkdir("/w", 0755))
+ require.NoError(t, m.WriteFile("/w/a.txt", []byte("1234"), 0644))
+ require.NoError(t, m.WriteFile("/w/sub/b.txt", []byte("567"), 0644))
+
+ // CalcTotalSize and CalcItemCount are pure Walk consumers — exercising them proves Walk
+ // works against a non-OS backend.
+ size, err := CalcTotalSize(m, "/w")
+ require.NoError(t, err)
+ assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x07}, size) // 4 + 3 bytes
+
+ count, err := CalcItemCount(m, "/w")
+ require.NoError(t, err)
+ // Walk visits /w, /w/a.txt, /w/sub, /w/sub/b.txt = 4 non-hidden entries, minus the root.
+ assert.Equal(t, []byte{0x00, 0x03}, count)
+}
+
+func TestMemFileStore_SymlinkUnsupported(t *testing.T) {
+ m := NewMemFileStore()
+ err := m.Symlink("/a", "/b")
+ assert.True(t, errors.Is(err, errors.ErrUnsupported))
+
+ _, err = m.ReadLink("/b")
+ assert.True(t, errors.Is(err, errors.ErrUnsupported))
+}
+
+// TestMemFileStore_DownloadRoundTrip proves the abstraction is genuinely swappable: a file
+// staged entirely in memory (no disk, no *os.File) is served through the real DownloadHandler.
+func TestMemFileStore_DownloadRoundTrip(t *testing.T) {
+ m := NewMemFileStore()
+
+ fileData := []byte("the quick brown fox")
+ require.NoError(t, m.WriteFile("/files/story.txt", fileData, 0644))
+
+ ft := &FileTransfer{bytesSentCounter: &WriteCounter{}}
+
+ var out bytes.Buffer
+ err := DownloadHandler(&out, "/files/story.txt", ft, m, slog.Default(), true)
+ require.NoError(t, err)
+
+ // The download stream is the flattened file object header followed by the data fork; the
+ // raw file bytes must appear in the output.
+ assert.True(t, bytes.Contains(out.Bytes(), fileData), "download output should contain the file data")
+ assert.Equal(t, int64(len(fileData)), ft.bytesSentCounter.Total)
+}
+
+// TestMemFileStore_GetFileNameList proves directory listing works against the in-memory backend.
+func TestMemFileStore_GetFileNameList(t *testing.T) {
+ m := NewMemFileStore()
+ require.NoError(t, m.WriteFile(filepath.Join("/library", "readme.txt"), []byte("12345"), 0644))
+
+ fields, err := GetFileNameList(m, "/library", nil, encoding.Nop.NewEncoder(), slog.Default())
+ require.NoError(t, err)
+ require.Len(t, fields, 1)
+
+ var fnwi FileNameWithInfo
+ _, err = fnwi.Write(fields[0].Data)
+ require.NoError(t, err)
+ assert.Equal(t, "readme.txt", string(fnwi.Name))
+}