package updater import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" ) // ─── FindClientAssets ─────────────────────────────────────────────────────── func TestFindClientAssets_NilCache(t *testing.T) { u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") ca := u.FindClientAssets("windows-x86_64-nsis") if ca.InstallerURL != "" || ca.SignatureURL != "" { t.Error("expected empty ClientAssets when no cache") } } func TestFindClientAssets_ByTarget(t *testing.T) { u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") u.mu.Lock() u.cache = &UpdateInfo{ Assets: []Asset{ {Name: "OwnCord_1.0.0_x64-setup.nsis.zip", DownloadURL: "https://example.com/installer.zip"}, {Name: "OwnCord_1.0.0_x64-setup.nsis.zip.sig", DownloadURL: "https://example.com/installer.zip.sig"}, {Name: "OwnCord_1.0.0_amd64.AppImage.tar.gz", DownloadURL: "https://example.com/amd64.AppImage.tar.gz"}, {Name: "OwnCord_1.0.0_amd64.AppImage.tar.gz.sig", DownloadURL: "https://example.com/amd64.AppImage.tar.gz.sig"}, {Name: "OwnCord_1.0.0_aarch64.AppImage.tar.gz", DownloadURL: "https://example.com/aarch64.AppImage.tar.gz"}, {Name: "OwnCord_1.0.0_aarch64.AppImage.tar.gz.sig", DownloadURL: "https://example.com/aarch64.AppImage.tar.gz.sig"}, {Name: "chatserver.exe", DownloadURL: "https://example.com/chatserver.exe"}, }, } u.mu.Unlock() cases := []struct { target string wantInstaller string wantSig string }{ {"windows-x86_64-nsis", "https://example.com/installer.zip", "https://example.com/installer.zip.sig"}, {"linux-x86_64-appimage", "https://example.com/amd64.AppImage.tar.gz", "https://example.com/amd64.AppImage.tar.gz.sig"}, {"linux-aarch64-appimage", "https://example.com/aarch64.AppImage.tar.gz", "https://example.com/aarch64.AppImage.tar.gz.sig"}, // No deb updater artifact is published — a deb client must get // nothing rather than an AppImage archive its installer rejects. {"linux-x86_64-deb", "", ""}, {"darwin-aarch64-app", "", ""}, {"windows-x86_64-unknown", "", ""}, {"", "", ""}, } for _, tc := range cases { ca := u.FindClientAssets(tc.target) if ca.InstallerURL != tc.wantInstaller || ca.SignatureURL != tc.wantSig { t.Errorf("FindClientAssets(%q) = {%q, %q}, want {%q, %q}", tc.target, ca.InstallerURL, ca.SignatureURL, tc.wantInstaller, tc.wantSig) } } } func TestFindClientAssets_NoMatchingAssets(t *testing.T) { u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") u.mu.Lock() u.cache = &UpdateInfo{ Assets: []Asset{ {Name: "chatserver.exe", DownloadURL: "https://example.com/chatserver.exe"}, {Name: "checksums.sha256", DownloadURL: "https://example.com/checksums.sha256"}, }, } u.mu.Unlock() ca := u.FindClientAssets("windows-x86_64-nsis") if ca.InstallerURL != "" || ca.SignatureURL != "" { t.Error("expected empty ClientAssets when no NSIS assets") } } // ─── FetchTextAsset ───────────────────────────────────────────────────────── func TestFetchTextAsset_Success(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("dW50cnVzdGVkIGNvbW1lbnQ=")) })) defer srv.Close() u := newTestUpdater(srv.URL, "1.0.0") text, err := u.FetchTextAsset(context.Background(), srv.URL+"/sig.txt") if err != nil { t.Fatalf("FetchTextAsset: %v", err) } if text != "dW50cnVzdGVkIGNvbW1lbnQ=" { t.Errorf("text = %q, want 'dW50cnVzdGVkIGNvbW1lbnQ='", text) } } func TestFetchTextAsset_Error(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() u := newTestUpdater(srv.URL, "1.0.0") _, err := u.FetchTextAsset(context.Background(), srv.URL+"/missing.sig") if err == nil { t.Error("expected error for 404 response") } } // ─── shouldSendToken ──────────────────────────────────────────────────────── func TestShouldSendToken_GitHubHost(t *testing.T) { u := NewUpdater("1.0.0", "tok", "J3vb", "OwnCord") tests := []struct { url string want bool }{ {"https://api.github.com/repos/foo/bar", true}, {"https://github.com/releases/download/v1", true}, {"https://objects.githubusercontent.com/asset", true}, {"https://evil.com/malicious", false}, {"https://notgithub.example.com/foo", false}, } for _, tc := range tests { got := u.shouldSendToken(tc.url) if got != tc.want { t.Errorf("shouldSendToken(%q) = %v, want %v", tc.url, got, tc.want) } } } func TestShouldSendToken_CustomBaseURL(t *testing.T) { u := NewUpdater("1.0.0", "tok", "J3vb", "OwnCord") u.baseURL = "http://localhost:9090" if !u.shouldSendToken("http://localhost:9090/repos/foo/bar") { t.Error("expected true for URL matching baseURL") } if u.shouldSendToken("http://localhost:8080/different") { t.Error("expected false for URL not matching baseURL") } } // ─── isGitHubHost ─────────────────────────────────────────────────────────── func TestIsGitHubHost(t *testing.T) { tests := []struct { url string want bool }{ {"https://api.github.com/repos", true}, {"https://github.com/J3vb/OwnCord", true}, {"https://objects.githubusercontent.com/asset", true}, {"https://raw.githubusercontent.com/file", true}, {"https://evil.com", false}, {"not a valid url \x00", false}, {"https://fakegithub.com", false}, } for _, tc := range tests { got := isGitHubHost(tc.url) if got != tc.want { t.Errorf("isGitHubHost(%q) = %v, want %v", tc.url, got, tc.want) } } } // ─── ensureVPrefix ────────────────────────────────────────────────────────── func TestEnsureVPrefix(t *testing.T) { tests := []struct { input string want string }{ {"1.0.0", "v1.0.0"}, {"v1.0.0", "v1.0.0"}, {"0.0.1", "v0.0.1"}, {"v0.0.1", "v0.0.1"}, } for _, tc := range tests { got := ensureVPrefix(tc.input) if got != tc.want { t.Errorf("ensureVPrefix(%q) = %q, want %q", tc.input, got, tc.want) } } } // ─── CheckForUpdate error caching ─────────────────────────────────────────── func TestCheckForUpdate_ErrorCaching(t *testing.T) { var hitCount int mux := http.NewServeMux() mux.HandleFunc("/repos/J3vb/OwnCord/releases/latest", func(w http.ResponseWriter, r *http.Request) { hitCount++ w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprint(w, `{"message":"error"}`) }) srv := httptest.NewServer(mux) defer srv.Close() u := newTestUpdater(srv.URL, "1.0.0") // First call should error and cache. _, err := u.CheckForUpdate(context.Background()) if err == nil { t.Fatal("expected error") } // Second call should use cached error. _, err = u.CheckForUpdate(context.Background()) if err == nil { t.Fatal("expected cached error") } if hitCount != 1 { t.Errorf("expected 1 API hit (error cached), got %d", hitCount) } } // ─── CheckForUpdate with assets list ──────────────────────────────────────── func TestCheckForUpdate_IncludesAssetsList(t *testing.T) { release := ghRelease{ TagName: "v2.0.0", Body: "notes", HTMLURL: "https://github.com/J3vb/OwnCord/releases/tag/v2.0.0", Assets: []ghAsset{ {Name: "chatserver.exe", BrowserDownloadURL: "https://example.com/chatserver.exe"}, {Name: "checksums.sha256", BrowserDownloadURL: "https://example.com/checksums.sha256"}, {Name: "OwnCord_2.0.0_x64-setup.nsis.zip", BrowserDownloadURL: "https://example.com/installer.zip"}, }, } mux := http.NewServeMux() mux.HandleFunc("/repos/J3vb/OwnCord/releases/latest", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(release) }) srv := httptest.NewServer(mux) defer srv.Close() u := newTestUpdater(srv.URL, "1.0.0") info, err := u.CheckForUpdate(context.Background()) if err != nil { t.Fatalf("CheckForUpdate: %v", err) } if len(info.Assets) != 3 { t.Errorf("expected 3 assets, got %d", len(info.Assets)) } } // ─── downloadFile with auth token ─────────────────────────────────────────── func TestDownloadFile_SendsTokenToGitHub(t *testing.T) { var gotAuth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("data")) })) defer srv.Close() u := NewUpdater("1.0.0", "my-secret-token", "J3vb", "OwnCord") u.baseURL = srv.URL dest := t.TempDir() + "/download.bin" err := u.downloadFile(context.Background(), srv.URL+"/file", dest) if err != nil { t.Fatalf("downloadFile: %v", err) } if gotAuth != "token my-secret-token" { t.Errorf("Authorization = %q, want 'token my-secret-token'", gotAuth) } } func TestDownloadFile_NoTokenToExternalHost(t *testing.T) { var gotAuth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotAuth = r.Header.Get("Authorization") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte("data")) })) defer srv.Close() u := NewUpdater("1.0.0", "my-secret-token", "J3vb", "OwnCord") // baseURL is NOT set to srv.URL, so shouldSendToken returns false. dest := t.TempDir() + "/download2.bin" err := u.downloadFile(context.Background(), srv.URL+"/file", dest) if err != nil { t.Fatalf("downloadFile: %v", err) } if gotAuth != "" { t.Errorf("expected no Authorization header for external host, got %q", gotAuth) } } // ─── ParseChecksumFile edge cases ─────────────────────────────────────────── func TestParseChecksumFile_SingleSpace(t *testing.T) { // Some tools output single-space instead of double-space. data := []byte("abc123 chatserver.exe\n") u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") hash, err := u.ParseChecksumFile(data, "chatserver.exe") if err != nil { t.Fatalf("ParseChecksumFile single space: %v", err) } if hash != "abc123" { t.Errorf("hash = %q, want 'abc123'", hash) } } func TestParseChecksumFile_EmptyLines(t *testing.T) { data := []byte("\n\nabc123 chatserver.exe\n\n") u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") hash, err := u.ParseChecksumFile(data, "chatserver.exe") if err != nil { t.Fatalf("ParseChecksumFile with empty lines: %v", err) } if hash != "abc123" { t.Errorf("hash = %q, want 'abc123'", hash) } } func TestParseChecksumFile_EmptyData(t *testing.T) { u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") _, err := u.ParseChecksumFile([]byte(""), "chatserver.exe") if err == nil { t.Error("expected error for empty checksum data") } } // ─── VerifyChecksum file not found ────────────────────────────────────────── func TestVerifyChecksum_FileNotFound(t *testing.T) { u := NewUpdater("1.0.0", "", "J3vb", "OwnCord") err := u.VerifyChecksum("/nonexistent/path/to/file.exe", "abc123") if err == nil { t.Error("expected error for non-existent file") } }