aboutsummaryrefslogtreecommitdiff
path: root/hotline/mem_file_store_test.go
blob: 71fa33910e3a201eabb1fc35c899c18c33cb31f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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))
}