podcast-go

TUI podcast downloader for Apple Podcasts
Log | Files | Refs | README | LICENSE

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&amp;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 }