mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
feat: add updater package with GitHub Release checking and checksum verification
This commit is contained in:
@@ -21,6 +21,12 @@ type Config struct {
|
||||
TLS TLSConfig `koanf:"tls"`
|
||||
Upload UploadConfig `koanf:"upload"`
|
||||
Voice VoiceConfig `koanf:"voice"`
|
||||
GitHub GitHubConfig `koanf:"github"`
|
||||
}
|
||||
|
||||
// GitHubConfig holds GitHub API settings for update checking.
|
||||
type GitHubConfig struct {
|
||||
Token string `koanf:"token"`
|
||||
}
|
||||
|
||||
// VoiceConfig holds STUN/TURN server settings for WebRTC signaling.
|
||||
@@ -80,6 +86,7 @@ func defaults() Config {
|
||||
TURNPort: 3478,
|
||||
TURNEnabled: true,
|
||||
},
|
||||
GitHub: GitHubConfig{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +109,9 @@ tls:
|
||||
upload:
|
||||
max_size_mb: 100
|
||||
storage_dir: "data/uploads"
|
||||
|
||||
# github:
|
||||
# token: "" # optional: GitHub API token for higher rate limits (5000 req/hr vs 60)
|
||||
`
|
||||
|
||||
// Load reads configuration from the given YAML file path, merging with
|
||||
|
||||
@@ -12,6 +12,7 @@ require (
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
go.yaml.in/yaml/v3 v3.0.3
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/mod v0.34.0
|
||||
modernc.org/sqlite v1.46.1
|
||||
nhooyr.io/websocket v1.8.17
|
||||
)
|
||||
|
||||
+4
-4
@@ -58,8 +58,8 @@ golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
@@ -67,8 +67,8 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
// Package updater checks GitHub Releases for server updates and manages
|
||||
// binary downloads with checksum verification.
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://api.github.com"
|
||||
cacheTTL = 1 * time.Hour
|
||||
binaryAsset = "chatserver.exe"
|
||||
checksumAsset = "checksums.sha256"
|
||||
)
|
||||
|
||||
// UpdateInfo holds the result of a version check.
|
||||
type UpdateInfo struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
ReleaseURL string `json:"release_url"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
ChecksumURL string `json:"checksum_url"`
|
||||
ReleaseNotes string `json:"release_notes"`
|
||||
}
|
||||
|
||||
// releaseResponse mirrors the subset of GitHub's release API we need.
|
||||
type releaseResponse struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Body string `json:"body"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Assets []assetResponse `json:"assets"`
|
||||
}
|
||||
|
||||
// assetResponse mirrors a single release asset from the GitHub API.
|
||||
type assetResponse struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
// Updater checks GitHub Releases for updates and manages binary downloads.
|
||||
type Updater struct {
|
||||
currentVersion string
|
||||
githubToken string
|
||||
repoOwner string
|
||||
repoName string
|
||||
baseURL string // override for testing; empty uses defaultBaseURL
|
||||
|
||||
cache *UpdateInfo
|
||||
cacheExpiry time.Time
|
||||
mu sync.Mutex
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewUpdater creates an Updater for the given repository.
|
||||
func NewUpdater(currentVersion, githubToken, repoOwner, repoName string) *Updater {
|
||||
return &Updater{
|
||||
currentVersion: currentVersion,
|
||||
githubToken: githubToken,
|
||||
repoOwner: repoOwner,
|
||||
repoName: repoName,
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
// ensureVPrefix returns the version string with a "v" prefix for semver
|
||||
// comparison. If it already has one, it is returned unchanged.
|
||||
func ensureVPrefix(v string) string {
|
||||
if strings.HasPrefix(v, "v") {
|
||||
return v
|
||||
}
|
||||
return "v" + v
|
||||
}
|
||||
|
||||
// apiBaseURL returns the effective base URL for GitHub API requests.
|
||||
func (u *Updater) apiBaseURL() string {
|
||||
if u.baseURL != "" {
|
||||
return u.baseURL
|
||||
}
|
||||
return defaultBaseURL
|
||||
}
|
||||
|
||||
// CheckForUpdate queries GitHub for the latest release and compares it
|
||||
// against the current version. Results are cached for cacheTTL.
|
||||
func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateInfo, error) {
|
||||
u.mu.Lock()
|
||||
if u.cache != nil && time.Now().Before(u.cacheExpiry) {
|
||||
cached := *u.cache
|
||||
u.mu.Unlock()
|
||||
return cached, nil
|
||||
}
|
||||
u.mu.Unlock()
|
||||
|
||||
url := fmt.Sprintf("%s/repos/%s/%s/releases/latest", u.apiBaseURL(), u.repoOwner, u.repoName)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return UpdateInfo{}, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
if u.githubToken != "" {
|
||||
req.Header.Set("Authorization", "token "+u.githubToken)
|
||||
}
|
||||
|
||||
resp, err := u.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return UpdateInfo{}, fmt.Errorf("fetching latest release: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return UpdateInfo{}, fmt.Errorf("github API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release releaseResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return UpdateInfo{}, fmt.Errorf("decoding release response: %w", err)
|
||||
}
|
||||
|
||||
currentV := ensureVPrefix(u.currentVersion)
|
||||
latestV := ensureVPrefix(release.TagName)
|
||||
|
||||
// semver.Compare returns -1, 0, or +1. Update available when current < latest.
|
||||
updateAvailable := semver.Compare(currentV, latestV) < 0
|
||||
|
||||
var downloadURL, checksumURL string
|
||||
for _, asset := range release.Assets {
|
||||
switch asset.Name {
|
||||
case binaryAsset:
|
||||
downloadURL = asset.BrowserDownloadURL
|
||||
case checksumAsset:
|
||||
checksumURL = asset.BrowserDownloadURL
|
||||
}
|
||||
}
|
||||
|
||||
info := UpdateInfo{
|
||||
Current: currentV,
|
||||
Latest: latestV,
|
||||
UpdateAvailable: updateAvailable,
|
||||
ReleaseURL: release.HTMLURL,
|
||||
DownloadURL: downloadURL,
|
||||
ChecksumURL: checksumURL,
|
||||
ReleaseNotes: release.Body,
|
||||
}
|
||||
|
||||
u.mu.Lock()
|
||||
u.cache = &info
|
||||
u.cacheExpiry = time.Now().Add(cacheTTL)
|
||||
u.mu.Unlock()
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// ValidateDownloadURL ensures the URL points to an expected GitHub release
|
||||
// asset for this repository.
|
||||
func (u *Updater) ValidateDownloadURL(url string) error {
|
||||
prefix := fmt.Sprintf("https://github.com/%s/%s/releases/download/", u.repoOwner, u.repoName)
|
||||
if !strings.HasPrefix(url, prefix) {
|
||||
return fmt.Errorf("download URL %q does not match expected prefix %q", url, prefix)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadAndVerify downloads the binary from downloadURL, fetches the
|
||||
// checksum file from checksumURL, and verifies the SHA256 hash matches.
|
||||
// On checksum mismatch the downloaded file is removed.
|
||||
func (u *Updater) DownloadAndVerify(ctx context.Context, downloadURL, checksumURL, destPath string) error {
|
||||
if err := u.ValidateDownloadURL(downloadURL); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch checksum file.
|
||||
checksumData, err := u.fetchBody(ctx, checksumURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching checksums: %w", err)
|
||||
}
|
||||
|
||||
destFilename := filepath.Base(destPath)
|
||||
expectedHash, err := u.ParseChecksumFile(checksumData, destFilename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing checksum file: %w", err)
|
||||
}
|
||||
|
||||
// Download the binary.
|
||||
if err := u.downloadFile(ctx, downloadURL, destPath); err != nil {
|
||||
return fmt.Errorf("downloading binary: %w", err)
|
||||
}
|
||||
|
||||
// Verify hash.
|
||||
if err := u.VerifyChecksum(destPath, expectedHash); err != nil {
|
||||
// Remove the invalid file.
|
||||
_ = os.Remove(destPath)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyChecksum computes the SHA256 hash of the file at filePath and
|
||||
// compares it (case-insensitive) against expectedHash.
|
||||
func (u *Updater) VerifyChecksum(filePath, expectedHash string) error {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening file for checksum: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return fmt.Errorf("computing checksum: %w", err)
|
||||
}
|
||||
|
||||
actual := hex.EncodeToString(h.Sum(nil))
|
||||
if !strings.EqualFold(actual, expectedHash) {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedHash, actual)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseChecksumFile parses a sha256sum-format checksum file (lines of
|
||||
// "<hash> <filename>") and returns the hash for the given filename.
|
||||
func (u *Updater) ParseChecksumFile(data []byte, filename string) (string, error) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// sha256sum format: "<hash> <filename>" (two spaces)
|
||||
// Also handle single-space separation for robustness.
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 && parts[len(parts)-1] == filename {
|
||||
return parts[0], nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("file %q not found in checksum data", filename)
|
||||
}
|
||||
|
||||
// fetchBody performs a GET request and returns the response body as bytes.
|
||||
func (u *Updater) fetchBody(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if u.githubToken != "" {
|
||||
req.Header.Set("Authorization", "token "+u.githubToken)
|
||||
}
|
||||
|
||||
resp, err := u.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP %d fetching %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// downloadFile downloads the content at url and writes it to destPath.
|
||||
func (u *Updater) downloadFile(ctx context.Context, url, destPath string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.githubToken != "" {
|
||||
req.Header.Set("Authorization", "token "+u.githubToken)
|
||||
}
|
||||
|
||||
resp, err := u.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HTTP %d downloading %s", resp.StatusCode, url)
|
||||
}
|
||||
|
||||
f, err := os.Create(destPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating destination file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
return fmt.Errorf("writing downloaded file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ghRelease mirrors the GitHub release API response shape.
|
||||
type ghRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Body string `json:"body"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Assets []ghAsset `json:"assets"`
|
||||
}
|
||||
|
||||
// ghAsset mirrors a GitHub release asset.
|
||||
type ghAsset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
func newTestRelease(tag, body, htmlURL string, assetDownloadBase string) ghRelease {
|
||||
return ghRelease{
|
||||
TagName: tag,
|
||||
Body: body,
|
||||
HTMLURL: htmlURL,
|
||||
Assets: []ghAsset{
|
||||
{Name: "chatserver.exe", BrowserDownloadURL: assetDownloadBase + "/chatserver.exe"},
|
||||
{Name: "checksums.sha256", BrowserDownloadURL: assetDownloadBase + "/checksums.sha256"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T, release ghRelease, statusCode int) *httptest.Server {
|
||||
t.Helper()
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/repos/J3vb/OwnCord/releases/latest", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
if statusCode == http.StatusOK {
|
||||
if err := json.NewEncoder(w).Encode(release); err != nil {
|
||||
t.Fatalf("encoding release: %v", err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprint(w, `{"message":"Internal Server Error"}`)
|
||||
}
|
||||
})
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func newTestUpdater(baseURL, currentVersion string) *Updater {
|
||||
u := NewUpdater(currentVersion, "", "J3vb", "OwnCord")
|
||||
u.baseURL = baseURL
|
||||
return u
|
||||
}
|
||||
|
||||
func TestCheckForUpdate_NewerVersionAvailable(t *testing.T) {
|
||||
release := newTestRelease("v1.2.0", "Bug fixes and improvements", "https://github.com/J3vb/OwnCord/releases/tag/v1.2.0",
|
||||
"https://github.com/J3vb/OwnCord/releases/download/v1.2.0")
|
||||
srv := newTestServer(t, release, http.StatusOK)
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
info, err := u.CheckForUpdate(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !info.UpdateAvailable {
|
||||
t.Error("expected UpdateAvailable=true, got false")
|
||||
}
|
||||
if info.Latest != "v1.2.0" {
|
||||
t.Errorf("expected Latest=v1.2.0, got %s", info.Latest)
|
||||
}
|
||||
if info.Current != "v1.0.0" {
|
||||
t.Errorf("expected Current=v1.0.0, got %s", info.Current)
|
||||
}
|
||||
if info.DownloadURL == "" {
|
||||
t.Error("expected non-empty DownloadURL")
|
||||
}
|
||||
if info.ChecksumURL == "" {
|
||||
t.Error("expected non-empty ChecksumURL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdate_UpToDate(t *testing.T) {
|
||||
release := newTestRelease("v1.0.0", "Current release", "https://github.com/J3vb/OwnCord/releases/tag/v1.0.0",
|
||||
"https://github.com/J3vb/OwnCord/releases/download/v1.0.0")
|
||||
srv := newTestServer(t, release, http.StatusOK)
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
info, err := u.CheckForUpdate(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if info.UpdateAvailable {
|
||||
t.Error("expected UpdateAvailable=false, got true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdate_CachesResult(t *testing.T) {
|
||||
var hitCount atomic.Int32
|
||||
release := newTestRelease("v2.0.0", "Major update", "https://github.com/J3vb/OwnCord/releases/tag/v2.0.0",
|
||||
"https://github.com/J3vb/OwnCord/releases/download/v2.0.0")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/repos/J3vb/OwnCord/releases/latest", func(w http.ResponseWriter, r *http.Request) {
|
||||
hitCount.Add(1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(release); err != nil {
|
||||
t.Fatalf("encoding release: %v", err)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := u.CheckForUpdate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("first call error: %v", err)
|
||||
}
|
||||
_, err = u.CheckForUpdate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("second call error: %v", err)
|
||||
}
|
||||
|
||||
if got := hitCount.Load(); got != 1 {
|
||||
t.Errorf("expected 1 API hit (cached), got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdate_CacheExpires(t *testing.T) {
|
||||
var hitCount atomic.Int32
|
||||
release := newTestRelease("v2.0.0", "Major update", "https://github.com/J3vb/OwnCord/releases/tag/v2.0.0",
|
||||
"https://github.com/J3vb/OwnCord/releases/download/v2.0.0")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/repos/J3vb/OwnCord/releases/latest", func(w http.ResponseWriter, r *http.Request) {
|
||||
hitCount.Add(1)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(release); err != nil {
|
||||
t.Fatalf("encoding release: %v", err)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
ctx := context.Background()
|
||||
|
||||
// First call populates cache.
|
||||
_, err := u.CheckForUpdate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("first call error: %v", err)
|
||||
}
|
||||
|
||||
// Expire the cache manually.
|
||||
u.mu.Lock()
|
||||
u.cacheExpiry = time.Now().Add(-1 * time.Minute)
|
||||
u.mu.Unlock()
|
||||
|
||||
// Second call should hit the API again.
|
||||
_, err = u.CheckForUpdate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("second call error: %v", err)
|
||||
}
|
||||
|
||||
if got := hitCount.Load(); got != 2 {
|
||||
t.Errorf("expected 2 API hits (cache expired), got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdate_APIError(t *testing.T) {
|
||||
release := ghRelease{} // unused since status is 500
|
||||
srv := newTestServer(t, release, http.StatusInternalServerError)
|
||||
defer srv.Close()
|
||||
|
||||
u := newTestUpdater(srv.URL, "1.0.0")
|
||||
_, err := u.CheckForUpdate(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 500 response, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDownloadURL_Valid(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
err := u.ValidateDownloadURL("https://github.com/J3vb/OwnCord/releases/download/v1.0.0/chatserver.exe")
|
||||
if err != nil {
|
||||
t.Errorf("expected valid URL to pass, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDownloadURL_Invalid(t *testing.T) {
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
err := u.ValidateDownloadURL("https://evil.com/chatserver.exe")
|
||||
if err == nil {
|
||||
t.Error("expected invalid URL to be rejected, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyChecksum_Correct(t *testing.T) {
|
||||
content := []byte("hello world binary content")
|
||||
hash := sha256.Sum256(content)
|
||||
expectedHash := hex.EncodeToString(hash[:])
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "chatserver.exe")
|
||||
if err := os.WriteFile(filePath, content, 0o644); err != nil {
|
||||
t.Fatalf("writing temp file: %v", err)
|
||||
}
|
||||
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
if err := u.VerifyChecksum(filePath, expectedHash); err != nil {
|
||||
t.Errorf("expected correct checksum to pass, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyChecksum_Incorrect(t *testing.T) {
|
||||
content := []byte("hello world binary content")
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
filePath := filepath.Join(tmpDir, "chatserver.exe")
|
||||
if err := os.WriteFile(filePath, content, 0o644); err != nil {
|
||||
t.Fatalf("writing temp file: %v", err)
|
||||
}
|
||||
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
wrongHash := "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
if err := u.VerifyChecksum(filePath, wrongHash); err == nil {
|
||||
t.Error("expected incorrect checksum to fail, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseChecksumFile_FindsFile(t *testing.T) {
|
||||
data := []byte("abc123 readme.txt\ndef456 chatserver.exe\nghi789 other.dll\n")
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
hash, err := u.ParseChecksumFile(data, "chatserver.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if hash != "def456" {
|
||||
t.Errorf("expected hash=def456, got %s", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseChecksumFile_FileNotFound(t *testing.T) {
|
||||
data := []byte("abc123 readme.txt\ndef456 chatserver.exe\n")
|
||||
u := NewUpdater("1.0.0", "", "J3vb", "OwnCord")
|
||||
_, err := u.ParseChecksumFile(data, "nonexistent.exe")
|
||||
if err == nil {
|
||||
t.Error("expected error for missing file in checksum data, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user