server_test.go
⎇
Raw
1package server
2
3import (
4 "io"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "vidarchive/internal/config"
12 "vidarchive/internal/database"
13 "vidarchive/internal/handler"
14 "vidarchive/internal/repository"
15 "vidarchive/internal/service"
16 "vidarchive/internal/worker"
17)
18
19func setupTestServer(t *testing.T) (*Server, *config.Config, func()) {
20 t.Helper()
21 dataDir := t.TempDir()
22 t.Setenv("VIDARCHIVE_DATA_DIR", dataDir)
23
24 cfg := config.New()
25 if err := os.MkdirAll(cfg.LibraryDir, 0755); err != nil {
26 t.Fatalf("create library dir: %v", err)
27 }
28 if err := os.MkdirAll(cfg.TempDir, 0755); err != nil {
29 t.Fatalf("create temp dir: %v", err)
30 }
31
32 db, err := database.New(cfg)
33 if err != nil {
34 t.Fatalf("init db: %v", err)
35 }
36
37 presetRepo := repository.NewPresetRepository(db)
38 downloadRepo := repository.NewDownloadRepository(db)
39 settingsRepo := repository.NewSettingsRepository(db)
40
41 presetSvc := service.NewPresetService(presetRepo)
42 librarySvc := service.NewLibraryService(cfg.LibraryDir, cfg.TempDir)
43 settingsSvc := service.NewSettingsService(settingsRepo)
44 downloadSvc := service.NewDownloadService(downloadRepo, librarySvc, presetSvc, settingsSvc, cfg)
45 workerPool := worker.New(downloadSvc, cfg.Workers)
46
47 h, err := handler.New(cfg, presetSvc, downloadSvc, librarySvc, settingsSvc, workerPool)
48 if err != nil {
49 t.Fatalf("init handler: %v", err)
50 }
51
52 srv := New(cfg, h)
53 cleanup := func() {
54 workerPool.Stop()
55 db.Close()
56 }
57 return srv, cfg, cleanup
58}
59
60func createItem(t *testing.T, libraryDir, relPath, name string, files map[string]string) {
61 t.Helper()
62 itemDir := filepath.Join(libraryDir, relPath)
63 if err := os.MkdirAll(itemDir, 0755); err != nil {
64 t.Fatalf("create item dir: %v", err)
65 }
66 marker := filepath.Join(itemDir, ".vidarchive-item.toml")
67 if err := os.WriteFile(marker, []byte("name = \""+name+"\"\nduration = -1\n"), 0644); err != nil {
68 t.Fatalf("write marker: %v", err)
69 }
70 for filename, content := range files {
71 path := filepath.Join(itemDir, filename)
72 if err := os.WriteFile(path, []byte(content), 0644); err != nil {
73 t.Fatalf("write file %s: %v", filename, err)
74 }
75 }
76}
77
78func TestNestedLibraryItem(t *testing.T) {
79 srv, cfg, cleanup := setupTestServer(t)
80 defer cleanup()
81
82 createItem(t, cfg.LibraryDir, "test/My Item [id]", "My Item", map[string]string{
83 "My Item [id].mp4": "dummy video",
84 })
85
86 router := srv.Router()
87
88 tests := []string{
89 "/library/item/test/My%20Item%20%5Bid%5D",
90 "/library/item/test/My+Item+%5Bid%5D",
91 }
92 for _, path := range tests {
93 req := httptest.NewRequest("GET", path, nil)
94 w := httptest.NewRecorder()
95 router.ServeHTTP(w, req)
96 if w.Code != http.StatusOK {
97 body, _ := io.ReadAll(w.Body)
98 t.Errorf("%s: expected 200, got %d: %s", path, w.Code, string(body))
99 }
100 }
101}
102
103func TestMediaFileQueryDecoding(t *testing.T) {
104 srv, cfg, cleanup := setupTestServer(t)
105 defer cleanup()
106
107 createItem(t, cfg.LibraryDir, "My Item [id]", "My Item", map[string]string{
108 "My Item [id].mp4": "dummy video",
109 "My+Other.mp4": "dummy video plus",
110 })
111
112 router := srv.Router()
113
114 tests := []struct {
115 path string
116 expected int
117 }{
118 {"/media/item/My%20Item%20%5Bid%5D?file=My+Item+%5Bid%5D.mp4", http.StatusOK},
119 {"/media/item/My%20Item%20%5Bid%5D?file=My%20Item%20%5Bid%5D.mp4", http.StatusOK},
120 {"/media/item/My%20Item%20%5Bid%5D?file=My%2BOther.mp4", http.StatusOK},
121 {"/media/item/My%20Item%20%5Bid%5D?file=missing.mp4", http.StatusNotFound},
122 }
123 for _, tc := range tests {
124 req := httptest.NewRequest("GET", tc.path, nil)
125 w := httptest.NewRecorder()
126 router.ServeHTTP(w, req)
127 if w.Code != tc.expected {
128 body, _ := io.ReadAll(w.Body)
129 t.Errorf("%s: expected %d, got %d: %s", tc.path, tc.expected, w.Code, string(body))
130 }
131 }
132}
133
134func TestPathTraversalBlocked(t *testing.T) {
135 srv, cfg, cleanup := setupTestServer(t)
136 defer cleanup()
137
138 outside := filepath.Join(cfg.DataDir, "secret")
139 if err := os.MkdirAll(outside, 0755); err != nil {
140 t.Fatalf("create outside dir: %v", err)
141 }
142 marker := filepath.Join(outside, ".vidarchive-item.toml")
143 if err := os.WriteFile(marker, []byte("name = \"secret\"\nduration = -1\n"), 0644); err != nil {
144 t.Fatalf("write marker: %v", err)
145 }
146
147 router := srv.Router()
148
149 req := httptest.NewRequest("GET", "/library/item/../secret", nil)
150 w := httptest.NewRecorder()
151 router.ServeHTTP(w, req)
152 if w.Code != http.StatusNotFound {
153 t.Errorf("expected 404 for path traversal, got %d", w.Code)
154 }
155}
156
157func TestExistingImageThumbnail(t *testing.T) {
158 srv, cfg, cleanup := setupTestServer(t)
159 defer cleanup()
160
161 createItem(t, cfg.LibraryDir, "thumb-item", "Thumb Item", map[string]string{
162 "cover.jpg": "fake image",
163 "video.mp4": "dummy video",
164 })
165
166 router := srv.Router()
167 req := httptest.NewRequest("GET", "/media/item/thumb-item/thumbnail", nil)
168 w := httptest.NewRecorder()
169 router.ServeHTTP(w, req)
170 if w.Code != http.StatusOK {
171 body, _ := io.ReadAll(w.Body)
172 t.Fatalf("expected 200, got %d: %s", w.Code, string(body))
173 }
174 if body, _ := io.ReadAll(w.Body); string(body) != "fake image" {
175 t.Errorf("expected cover.jpg contents, got %q", string(body))
176 }
177}
178
179func TestAudioThumbnailPlaceholder(t *testing.T) {
180 srv, cfg, cleanup := setupTestServer(t)
181 defer cleanup()
182
183 createItem(t, cfg.LibraryDir, "audio-item", "Audio Item", map[string]string{
184 "song.mp3": "dummy audio",
185 })
186
187 router := srv.Router()
188 req := httptest.NewRequest("GET", "/media/item/audio-item/thumbnail", nil)
189 w := httptest.NewRecorder()
190 router.ServeHTTP(w, req)
191 if w.Code != http.StatusOK {
192 body, _ := io.ReadAll(w.Body)
193 t.Fatalf("expected 200, got %d: %s", w.Code, string(body))
194 }
195 body, _ := io.ReadAll(w.Body)
196 if len(body) == 0 {
197 t.Errorf("placeholder thumbnail body was empty")
198 }
199}
200
201func TestLibraryPageIsFast(t *testing.T) {
202 srv, cfg, cleanup := setupTestServer(t)
203 defer cleanup()
204
205 for i := 0; i < 50; i++ {
206 createItem(t, cfg.LibraryDir, "item-"+string(rune('a'+i)), "Item", map[string]string{
207 "video.mp4": "dummy",
208 })
209 }
210
211 router := srv.Router()
212 req := httptest.NewRequest("GET", "/library", nil)
213 w := httptest.NewRecorder()
214 router.ServeHTTP(w, req)
215 if w.Code != http.StatusOK {
216 body, _ := io.ReadAll(w.Body)
217 t.Fatalf("expected 200, got %d: %s", w.Code, string(body))
218 }
219}
220