utils_test.go (7476B)
1 package podcast 2 3 import ( 4 "net/http" 5 "net/http/httptest" 6 "os" 7 "path/filepath" 8 "strings" 9 "testing" 10 ) 11 12 func TestIsNumeric(t *testing.T) { 13 tests := []struct { 14 input string 15 expected bool 16 }{ 17 {"123456", true}, 18 {"1200361736", true}, 19 {"0", true}, 20 {"", false}, 21 {"abc", false}, 22 {"123abc", false}, 23 {"12.34", false}, 24 {"-123", false}, 25 {"id123", false}, 26 } 27 28 for _, tt := range tests { 29 t.Run(tt.input, func(t *testing.T) { 30 result := IsNumeric(tt.input) 31 if result != tt.expected { 32 t.Errorf("IsNumeric(%q) = %v, want %v", tt.input, result, tt.expected) 33 } 34 }) 35 } 36 } 37 38 func TestSanitizeFilename(t *testing.T) { 39 tests := []struct { 40 input string 41 expected string 42 }{ 43 {"Simple Title", "Simple Title"}, 44 {"Title: With Colon", "Title With Colon"}, 45 {"Title/With/Slashes", "TitleWithSlashes"}, 46 {"Title\\With\\Backslashes", "TitleWithBackslashes"}, 47 {"Title<With>Brackets", "TitleWithBrackets"}, 48 {"Title|With|Pipes", "TitleWithPipes"}, 49 {"Title?With?Questions", "TitleWithQuestions"}, 50 {"Title*With*Stars", "TitleWithStars"}, 51 {"Title\"With\"Quotes", "TitleWithQuotes"}, 52 {" Spaces Around ", "Spaces Around"}, 53 {"", "episode"}, 54 {" ", "episode"}, 55 } 56 57 for _, tt := range tests { 58 t.Run(tt.input, func(t *testing.T) { 59 result := SanitizeFilename(tt.input) 60 if result != tt.expected { 61 t.Errorf("SanitizeFilename(%q) = %q, want %q", tt.input, result, tt.expected) 62 } 63 }) 64 } 65 } 66 67 func TestSanitizeFilename_LongName(t *testing.T) { 68 longName := strings.Repeat("a", 150) 69 result := SanitizeFilename(longName) 70 if len(result) > 100 { 71 t.Errorf("SanitizeFilename should truncate to 100 chars, got %d", len(result)) 72 } 73 if len(result) != 100 { 74 t.Errorf("SanitizeFilename(%d chars) = %d chars, want 100", len(longName), len(result)) 75 } 76 } 77 78 func TestDownloadFile_TextRedirect(t *testing.T) { 79 // Mock server that returns a text-based redirect 80 redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 81 w.Header().Set("Content-Type", "audio/mpeg") 82 w.Write([]byte("fake audio content")) 83 })) 84 defer redirectServer.Close() 85 86 mainServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 87 w.Header().Set("Content-Type", "text/plain") 88 w.Write([]byte("Redirecting to " + redirectServer.URL)) 89 })) 90 defer mainServer.Close() 91 92 // Create temp file 93 tmpDir := t.TempDir() 94 tmpFile := filepath.Join(tmpDir, "test.mp3") 95 96 err := DownloadFile(tmpFile, mainServer.URL, nil) 97 if err != nil { 98 t.Fatalf("DownloadFile failed: %v", err) 99 } 100 101 // Verify file was created with correct content 102 content, err := os.ReadFile(tmpFile) 103 if err != nil { 104 t.Fatalf("Failed to read file: %v", err) 105 } 106 107 if string(content) != "fake audio content" { 108 t.Errorf("File content = %q, want %q", string(content), "fake audio content") 109 } 110 } 111 112 func TestDownloadFile_HTMLEntityDecode(t *testing.T) { 113 // Mock server that returns a text-based redirect with HTML entities 114 redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 115 // Verify the query params were properly decoded 116 if r.URL.Query().Get("foo") != "bar" { 117 t.Errorf("Query param foo = %q, want %q", r.URL.Query().Get("foo"), "bar") 118 } 119 w.Header().Set("Content-Type", "audio/mpeg") 120 w.Write([]byte("audio with params")) 121 })) 122 defer redirectServer.Close() 123 124 mainServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 125 w.Header().Set("Content-Type", "text/plain") 126 // Simulate Acast's HTML-encoded ampersands 127 w.Write([]byte("Redirecting to " + redirectServer.URL + "?foo=bar&baz=qux")) 128 })) 129 defer mainServer.Close() 130 131 tmpDir := t.TempDir() 132 tmpFile := filepath.Join(tmpDir, "test.mp3") 133 134 err := DownloadFile(tmpFile, mainServer.URL, nil) 135 if err != nil { 136 t.Fatalf("DownloadFile failed: %v", err) 137 } 138 139 content, err := os.ReadFile(tmpFile) 140 if err != nil { 141 t.Fatalf("Failed to read file: %v", err) 142 } 143 144 if string(content) != "audio with params" { 145 t.Errorf("File content = %q, want %q", string(content), "audio with params") 146 } 147 } 148 149 func TestDownloadFile_DirectDownload(t *testing.T) { 150 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 151 w.Header().Set("Content-Type", "audio/mpeg") 152 w.Header().Set("Content-Length", "13") 153 w.Write([]byte("audio content")) 154 })) 155 defer server.Close() 156 157 tmpDir := t.TempDir() 158 tmpFile := filepath.Join(tmpDir, "test.mp3") 159 160 err := DownloadFile(tmpFile, server.URL, nil) 161 if err != nil { 162 t.Fatalf("DownloadFile failed: %v", err) 163 } 164 165 content, err := os.ReadFile(tmpFile) 166 if err != nil { 167 t.Fatalf("Failed to read file: %v", err) 168 } 169 170 if string(content) != "audio content" { 171 t.Errorf("File content = %q, want %q", string(content), "audio content") 172 } 173 } 174 175 func TestDownloadFile_SkipExisting(t *testing.T) { 176 callCount := 0 177 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 178 callCount++ 179 w.Write([]byte("new content")) 180 })) 181 defer server.Close() 182 183 tmpDir := t.TempDir() 184 tmpFile := filepath.Join(tmpDir, "existing.mp3") 185 186 // Create existing file 187 err := os.WriteFile(tmpFile, []byte("original content"), 0644) 188 if err != nil { 189 t.Fatalf("Failed to create file: %v", err) 190 } 191 192 err = DownloadFile(tmpFile, server.URL, nil) 193 if err != nil { 194 t.Fatalf("DownloadFile failed: %v", err) 195 } 196 197 // Should not have made a request 198 if callCount != 0 { 199 t.Errorf("Server was called %d times, want 0 (should skip existing)", callCount) 200 } 201 202 // Content should be unchanged 203 content, err := os.ReadFile(tmpFile) 204 if err != nil { 205 t.Fatalf("Failed to read file: %v", err) 206 } 207 208 if string(content) != "original content" { 209 t.Errorf("File content = %q, want %q", string(content), "original content") 210 } 211 } 212 213 func TestDownloadFile_ProgressCallback(t *testing.T) { 214 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 215 w.Header().Set("Content-Type", "audio/mpeg") 216 w.Header().Set("Content-Length", "100") 217 // Write 100 bytes 218 w.Write([]byte(strings.Repeat("x", 100))) 219 })) 220 defer server.Close() 221 222 tmpDir := t.TempDir() 223 tmpFile := filepath.Join(tmpDir, "test.mp3") 224 225 var lastProgress float64 226 progressCalled := false 227 228 err := DownloadFile(tmpFile, server.URL, func(percent float64) { 229 progressCalled = true 230 lastProgress = percent 231 }) 232 if err != nil { 233 t.Fatalf("DownloadFile failed: %v", err) 234 } 235 236 if !progressCalled { 237 t.Error("Progress callback was never called") 238 } 239 240 if lastProgress < 0.99 { 241 t.Errorf("Last progress = %v, want >= 0.99", lastProgress) 242 } 243 } 244 245 func TestParseEpisodeSpec(t *testing.T) { 246 tests := []struct { 247 spec string 248 total int 249 expected []int 250 }{ 251 {"", 5, []int{1, 2, 3, 4, 5}}, 252 {"all", 3, []int{1, 2, 3}}, 253 {"latest", 10, []int{1}}, 254 {"1", 10, []int{1}}, 255 {"5", 10, []int{5}}, 256 {"1,3,5", 10, []int{1, 3, 5}}, 257 {"1-3", 10, []int{1, 2, 3}}, 258 {"1-5", 10, []int{1, 2, 3, 4, 5}}, 259 {"1,3-5,7", 10, []int{1, 3, 4, 5, 7}}, 260 {"1, 2, 3", 10, []int{1, 2, 3}}, // spaces 261 } 262 263 for _, tt := range tests { 264 t.Run(tt.spec, func(t *testing.T) { 265 result := ParseEpisodeSpec(tt.spec, tt.total) 266 if len(result) != len(tt.expected) { 267 t.Errorf("ParseEpisodeSpec(%q, %d) = %v, want %v", tt.spec, tt.total, result, tt.expected) 268 return 269 } 270 for i := range result { 271 if result[i] != tt.expected[i] { 272 t.Errorf("ParseEpisodeSpec(%q, %d) = %v, want %v", tt.spec, tt.total, result, tt.expected) 273 break 274 } 275 } 276 }) 277 } 278 }