package version import ( "bytes" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetCurrentVersion(t *testing.T) { version := GetCurrentVersion() assert.NotEmpty(t, version) assert.Equal(t, CurrentVersion, version) } func TestCompareVersions(t *testing.T) { tests := []struct { name string v1 string v2 string want int }{ { name: "v1 less than v2", v1: "0.1.0", v2: "0.2.0", want: -1, }, { name: "v1 greater than v2", v1: "1.0.0", v2: "0.9.0", want: 1, }, { name: "v1 equal to v2", v1: "1.0.0", v2: "1.0.0", want: 0, }, { name: "with v prefix", v1: "v0.1.0", v2: "v0.2.0", want: -1, }, { name: "mixed v prefix", v1: "0.1.0", v2: "v0.2.0", want: -1, }, { name: "different major version", v1: "1.0.0", v2: "2.0.0", want: -1, }, { name: "different minor version", v1: "1.0.0", v2: "1.1.0", want: -1, }, { name: "different patch version", v1: "1.0.0", v2: "1.0.1", want: -1, }, { name: "v1 has more parts", v1: "1.0.0.1", v2: "1.0.0", want: 1, }, { name: "v2 has more parts", v1: "1.0.0", v2: "1.0.0.1", want: -1, }, { name: "empty strings", v1: "", v2: "", want: 0, }, { name: "v1 empty", v1: "", v2: "1.0.0", want: -1, }, { name: "v2 empty", v1: "1.0.0", v2: "", want: 1, }, { name: "non-numeric parts", v1: "1.0.0-alpha", v2: "1.0.0", want: 0, }, { name: "large version numbers", v1: "100.200.300", v2: "99.201.300", want: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := CompareVersions(tt.v1, tt.v2) assert.Equal(t, tt.want, got) }) } } func TestIsVersionUpdateAvailable(t *testing.T) { tests := []struct { name string current string latest string wantUpdate bool }{ { name: "update available", current: "0.1.0", latest: "0.2.0", wantUpdate: true, }, { name: "no update available", current: "1.0.0", latest: "0.9.0", wantUpdate: false, }, { name: "same version", current: "1.0.0", latest: "1.0.0", wantUpdate: false, }, { name: "major version update", current: "1.0.0", latest: "2.0.0", wantUpdate: true, }, { name: "patch update", current: "1.0.0", latest: "1.0.1", wantUpdate: true, }, { name: "with v prefix", current: "v0.1.0", latest: "v0.2.0", wantUpdate: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := IsVersionUpdateAvailable(tt.current, tt.latest) assert.Equal(t, tt.wantUpdate, got) }) } } func TestCheckForUpdate(t *testing.T) { t.Run("check for update", func(t *testing.T) { result := CheckForUpdate() assert.NotNil(t, result) assert.Equal(t, CurrentVersion, result.CurrentVersion) if result.Error != nil { assert.Contains(t, result.Error.Error(), "failed to check for updates") } }) } func TestVersionCheckResult(t *testing.T) { t.Run("create version check result", func(t *testing.T) { result := &VersionCheckResult{ CurrentVersion: "0.1.0", LatestVersion: "0.2.0", MpvVersion: "0.36.0", UpdateAvailable: true, URL: "https://example.com/update", BLAKE3: "blake3:abc123", } assert.Equal(t, "0.1.0", result.CurrentVersion) assert.Equal(t, "0.2.0", result.LatestVersion) assert.Equal(t, "0.36.0", result.MpvVersion) assert.True(t, result.UpdateAvailable) assert.Equal(t, "https://example.com/update", result.URL) assert.Equal(t, "blake3:abc123", result.BLAKE3) assert.Nil(t, result.Error) }) } func TestProgressWriter(t *testing.T) { t.Run("Write updates written field", func(t *testing.T) { var buf bytes.Buffer pw := &progressWriter{ total: 100, underlying: &buf, } n, err := pw.Write([]byte("hello")) assert.NoError(t, err) assert.Equal(t, 5, n) assert.Equal(t, int64(5), pw.written) assert.Equal(t, "hello", buf.String()) }) t.Run("Callback is called when throttle exceeded", func(t *testing.T) { var buf bytes.Buffer callbackCalled := 0 var lastWritten, lastTotal int64 pw := &progressWriter{ total: 100, underlying: &buf, callback: func(written, total int64) { callbackCalled++ lastWritten = written lastTotal = total }, lastUpdate: time.Now().Add(-600 * time.Millisecond), } pw.Write([]byte("test")) assert.Equal(t, 1, callbackCalled) assert.Equal(t, int64(4), lastWritten) assert.Equal(t, int64(100), lastTotal) }) t.Run("Callback is not called when throttle not exceeded", func(t *testing.T) { var buf bytes.Buffer callbackCalled := false pw := &progressWriter{ total: 100, underlying: &buf, callback: func(written, total int64) { callbackCalled = true }, lastUpdate: time.Now(), } pw.Write([]byte("test")) assert.False(t, callbackCalled) }) t.Run("Callback called when download completes", func(t *testing.T) { var buf bytes.Buffer callbackCalled := false pw := &progressWriter{ total: 4, underlying: &buf, callback: func(written, total int64) { callbackCalled = true }, lastUpdate: time.Now(), } pw.Write([]byte("test")) assert.True(t, callbackCalled) }) t.Run("Error propagates from underlying writer", func(t *testing.T) { pw := &progressWriter{ total: 100, underlying: &errorWriter{}, callback: func(written, total int64) {}, } _, err := pw.Write([]byte("test")) assert.Error(t, err) assert.Equal(t, "simulated write error", err.Error()) }) t.Run("callback called even on subsequent write error", func(t *testing.T) { callbackCalled := false pw := &progressWriter{ total: 100, underlying: &errorWriter{}, callback: func(written, total int64) { callbackCalled = true }, lastUpdate: time.Now().Add(-600 * time.Millisecond), } _, err := pw.Write([]byte("test")) assert.Error(t, err) assert.True(t, callbackCalled, "callback should be called with progress before error") }) } type errorWriter struct{} func (e *errorWriter) Write(p []byte) (n int, err error) { return 0, fmt.Errorf("simulated write error") } func TestDownloadFileWithProgress(t *testing.T) { t.Run("successful download with progress", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("test file content")) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "test-file.bin") progressUpdates := []struct{ written, total int64 }{} err := downloadFileWithProgress(server.URL, destPath, func(written, total int64) { progressUpdates = append(progressUpdates, struct{ written, total int64 }{written, total}) }, 3) assert.NoError(t, err) assert.FileExists(t, destPath) content, _ := os.ReadFile(destPath) assert.Equal(t, "test file content", string(content)) assert.NotEmpty(t, progressUpdates, "progress callback should be called") }) t.Run("retry on connection error", func(t *testing.T) { attempts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts++ if attempts < 3 { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte("success")) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "retry-test.bin") err := downloadFileWithProgress(server.URL, destPath, nil, 3) assert.NoError(t, err) assert.Equal(t, 3, attempts) content, _ := os.ReadFile(destPath) assert.Equal(t, "success", string(content)) }) t.Run("max retries exceeded", func(t *testing.T) { attempts := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { attempts++ w.WriteHeader(http.StatusInternalServerError) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "fail-test.bin") err := downloadFileWithProgress(server.URL, destPath, nil, 3) assert.Error(t, err) assert.Contains(t, err.Error(), "download failed after 3 attempts") assert.Equal(t, 3, attempts) }) t.Run("HTTP 404 error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "404-test.bin") err := downloadFileWithProgress(server.URL, destPath, nil, 1) assert.Error(t, err) assert.Contains(t, err.Error(), "download failed") }) t.Run("file write error", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("test content")) })) defer server.Close() // Create a temporary directory with a subdirectory tmpDir := t.TempDir() dirPath := filepath.Join(tmpDir, "directory") err := os.Mkdir(dirPath, 0755) require.NoError(t, err) // Try to write to a path that's a directory (will fail) // os.Create() cannot create a file if a directory with that name already exists destPath := dirPath err = downloadFileWithProgress(server.URL, destPath, nil, 1) assert.Error(t, err) assert.Contains(t, err.Error(), "is a directory") }) t.Run("exponential backoff delays", func(t *testing.T) { attempts := 0 startTimes := []time.Time{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { startTimes = append(startTimes, time.Now()) attempts++ if attempts < 4 { w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write([]byte("success")) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "backoff-test.bin") _ = downloadFileWithProgress(server.URL, destPath, nil, 4) assert.Equal(t, 4, attempts) if len(startTimes) >= 2 { delay1 := startTimes[1].Sub(startTimes[0]) assert.True(t, delay1 >= 900*time.Millisecond && delay1 <= 1100*time.Millisecond, fmt.Sprintf("first delay should be ~1s, got %v", delay1)) } if len(startTimes) >= 3 { delay2 := startTimes[2].Sub(startTimes[1]) assert.True(t, delay2 >= 1900*time.Millisecond && delay2 <= 2100*time.Millisecond, fmt.Sprintf("second delay should be ~2s, got %v", delay2)) } }) t.Run("progress callback frequency", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(strings.Repeat("test", 100))) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "progress-test.bin") progressCount := 0 _ = downloadFileWithProgress(server.URL, destPath, func(written, total int64) { progressCount++ }, 1) assert.Greater(t, progressCount, 0, "progress callback should be called at least once") }) t.Run("empty download", func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer server.Close() tmpDir := t.TempDir() destPath := filepath.Join(tmpDir, "empty-test.bin") progressUpdates := []struct{ written, total int64 }{} err := downloadFileWithProgress(server.URL, destPath, func(written, total int64) { progressUpdates = append(progressUpdates, struct{ written, total int64 }{written, total}) }, 1) assert.NoError(t, err) assert.FileExists(t, destPath) content, _ := os.ReadFile(destPath) assert.Equal(t, 0, len(content)) assert.Empty(t, progressUpdates) }) } func TestUpdateSelfWithProgress(t *testing.T) { t.Run("function accepts callback parameter", func(t *testing.T) { tmpDir := t.TempDir() executablePath := filepath.Join(tmpDir, "mpv-manager-test") err := UpdateSelfWithProgress(executablePath, func(written, total int64) {}) assert.Error(t, err) }) t.Run("callback receives int64 parameters", func(t *testing.T) { tmpDir := t.TempDir() executablePath := filepath.Join(tmpDir, "mpv-manager-test") err := UpdateSelfWithProgress(executablePath, func(written, total int64) { assert.IsType(t, int64(0), written) assert.IsType(t, int64(0), total) }) assert.Error(t, err) }) }