package httpapi import ( "encoding/json" "net/http/httptest" "os" "path/filepath" "testing" "github.com/go-chi/chi/v5" ) // buildVersionRouter mounts VersionHandler on a real chi router (mirrors how // main.go mounts GET /version) so routing behaves exactly as in production. func buildVersionRouter(path string) chi.Router { h := NewVersionHandler(path) r := chi.NewRouter() r.Get("/version", h.Serve) return r } func TestVersionHandler_ServesManifestFromDisk(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "version.yaml") yamlContent := `version: "1.2.3" build_number: 10203 force_update: true release_notes: "测试发布说明" download_urls: android: "https://api.yanmeiai.com/downloads/pangolin-android.apk" windows: "https://api.yanmeiai.com/downloads/pangolin-windows-x64-setup.exe" macos: "" ios: "" changelog: - version: "1.2.3" date: "2026-07-06" intro: "小版本更新" sections: - type: "新增" items: - "示例条目" ` if err := os.WriteFile(path, []byte(yamlContent), 0o644); err != nil { t.Fatalf("write fixture: %v", err) } r := buildVersionRouter(path) req := httptest.NewRequest("GET", "/version", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String()) } var got versionManifest if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("unmarshal response: %v; body=%s", err, rec.Body.String()) } if got.Version != "1.2.3" { t.Errorf("version = %q, want %q", got.Version, "1.2.3") } if got.BuildNumber != 10203 { t.Errorf("build_number = %d, want %d", got.BuildNumber, 10203) } if !got.ForceUpdate { t.Errorf("force_update = false, want true") } if got.ReleaseNotes != "测试发布说明" { t.Errorf("release_notes = %q, want %q", got.ReleaseNotes, "测试发布说明") } if got.DownloadURLs["android"] != "https://api.yanmeiai.com/downloads/pangolin-android.apk" { t.Errorf("download_urls.android = %q", got.DownloadURLs["android"]) } if got.DownloadURLs["windows"] != "https://api.yanmeiai.com/downloads/pangolin-windows-x64-setup.exe" { t.Errorf("download_urls.windows = %q", got.DownloadURLs["windows"]) } if got.DownloadURLs["macos"] != "" || got.DownloadURLs["ios"] != "" { t.Errorf("expected empty macos/ios download URLs, got macos=%q ios=%q", got.DownloadURLs["macos"], got.DownloadURLs["ios"]) } if len(got.Changelog) != 1 || got.Changelog[0].Version != "1.2.3" { t.Errorf("changelog = %+v, want 1 entry for version 1.2.3", got.Changelog) } } func TestVersionHandler_MissingFileReturnsDefault(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "does-not-exist.yaml") r := buildVersionRouter(path) req := httptest.NewRequest("GET", "/version", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) if rec.Code != 200 { t.Fatalf("status = %d, want 200 (missing file falls back to default manifest); body=%s", rec.Code, rec.Body.String()) } var got versionManifest if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("unmarshal response: %v; body=%s", err, rec.Body.String()) } if got.Version == "" { t.Errorf("default manifest: version should not be empty") } if got.DownloadURLs["android"] == "" { t.Errorf("default manifest: download_urls.android should not be empty") } if got.DownloadURLs["windows"] == "" { t.Errorf("default manifest: download_urls.windows should not be empty") } if got.Changelog == nil { t.Errorf("default manifest: changelog should be [] not null") } } func TestVersionHandler_JSONShape(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "version.yaml") if err := os.WriteFile(path, []byte(`version: "1.0.0" build_number: 10000 force_update: false release_notes: "" download_urls: android: "https://example.com/a.apk" `), 0o644); err != nil { t.Fatalf("write fixture: %v", err) } r := buildVersionRouter(path) req := httptest.NewRequest("GET", "/version", nil) rec := httptest.NewRecorder() r.ServeHTTP(rec, req) var raw map[string]json.RawMessage if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil { t.Fatalf("unmarshal raw response: %v", err) } for _, key := range []string{"version", "build_number", "force_update", "release_notes", "download_urls", "changelog"} { if _, ok := raw[key]; !ok { t.Errorf("response missing expected top-level key %q; body=%s", key, rec.Body.String()) } } // changelog must serialize as an array even when the manifest omits it — // front-ends should be able to blindly .map()/range over it. if string(raw["changelog"]) != "[]" { t.Errorf("changelog = %s, want []", raw["changelog"]) } if ct := rec.Header().Get("Content-Type"); ct != "application/json; charset=utf-8" { t.Errorf("Content-Type = %q, want application/json; charset=utf-8", ct) } } func TestVersionHandler_DefaultPathFallback(t *testing.T) { h := NewVersionHandler("") if h.path != defaultVersionManifestPath { t.Errorf("NewVersionHandler(\"\").path = %q, want %q", h.path, defaultVersionManifestPath) } }