package api_test import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" "github.com/owncord/server/api" "github.com/owncord/server/updater" ) // fakeGitHubRelease returns a test HTTP server that mimics the GitHub // Releases API, serving a release with the given tag and the client update // assets for every platform the release workflow publishes. // Asset download URLs point back to the test server so FetchTextAsset works. func fakeGitHubRelease(t *testing.T, tag string) *httptest.Server { t.Helper() var srv *httptest.Server mux := http.NewServeMux() assetNames := []string{ "OwnCord_1.0.0_x64-setup.nsis.zip", "OwnCord_1.0.0_x64-setup.nsis.zip.sig", "OwnCord_1.0.0_amd64.AppImage.tar.gz", "OwnCord_1.0.0_amd64.AppImage.tar.gz.sig", "OwnCord_1.0.0_aarch64.AppImage.tar.gz", "OwnCord_1.0.0_aarch64.AppImage.tar.gz.sig", } mux.HandleFunc("/repos/test/repo/releases/latest", func(w http.ResponseWriter, r *http.Request) { assets := make([]map[string]any, 0, len(assetNames)) for _, name := range assetNames { assets = append(assets, map[string]any{ "name": name, "browser_download_url": srv.URL + "/download/" + name, }) } resp := map[string]any{ "tag_name": tag, "body": "Release notes here", "html_url": "https://github.com/test/repo/releases/" + tag, "assets": assets, } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(resp) }) // Serve signature file content for any .sig asset. mux.HandleFunc("/download/", func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, ".sig") { _, _ = w.Write([]byte("dW50cnVzdGVkIGNvbW1lbnQ=")) return } http.NotFound(w, r) }) srv = httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } // platformEntry decodes the response body and returns the platforms entry for // the given target, failing the test if it is missing. func platformEntry(t *testing.T, rr *httptest.ResponseRecorder, target string) map[string]any { t.Helper() var resp map[string]any if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } platforms, ok := resp["platforms"].(map[string]any) if !ok { t.Fatalf("response missing platforms map: %v", resp) } entry, ok := platforms[target].(map[string]any) if !ok { t.Fatalf("platforms missing key %q: %v", target, platforms) } return entry } func buildClientUpdateRouter(u *updater.Updater) http.Handler { r := chi.NewRouter() api.MountClientUpdateRoute(r, u) return r } func TestClientUpdate_NewVersionAvailable(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String()) } var resp map[string]any if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } if resp["version"] == nil { t.Error("response missing 'version' field") } if resp["platforms"] == nil { t.Error("response missing 'platforms' field") } } func TestClientUpdate_AlreadyLatest(t *testing.T) { srv := fakeGitHubRelease(t, "v1.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String()) } } func TestClientUpdate_FutureVersion(t *testing.T) { srv := fakeGitHubRelease(t, "v1.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) // Client has a newer version than the release. req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/2.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String()) } } func TestClientUpdate_WindowsTargetGetsNSISInstaller(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String()) } entry := platformEntry(t, rr, "windows-x86_64-nsis") url, _ := entry["url"].(string) if !strings.HasSuffix(url, "_x64-setup.nsis.zip") { t.Errorf("windows url = %q, want NSIS installer", url) } } func TestClientUpdate_LinuxTargetGetsAppImage(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-x86_64-appimage/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String()) } entry := platformEntry(t, rr, "linux-x86_64-appimage") url, _ := entry["url"].(string) if !strings.HasSuffix(url, "_amd64.AppImage.tar.gz") { t.Errorf("linux url = %q, want x86_64 AppImage updater archive", url) } if sig, _ := entry["signature"].(string); sig == "" { t.Error("linux platform entry missing signature") } } func TestClientUpdate_LinuxArm64TargetGetsAarch64AppImage(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-aarch64-appimage/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusOK { t.Fatalf("status = %d, want 200; body: %s", rr.Code, rr.Body.String()) } entry := platformEntry(t, rr, "linux-aarch64-appimage") url, _ := entry["url"].(string) if !strings.HasSuffix(url, "_aarch64.AppImage.tar.gz") { t.Errorf("linux arm64 url = %q, want aarch64 AppImage updater archive", url) } } func TestClientUpdate_UnsupportedTargetNoContent(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) // No darwin client is published; the updater must not be offered a // Windows installer for it. req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/darwin-aarch64-app/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String()) } } func TestClientUpdate_DebTargetNoContent(t *testing.T) { srv := fakeGitHubRelease(t, "v2.0.0") u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) // The release ships .deb packages but no signed deb UPDATER artifact. // A deb client falling back to the AppImage archive would fail install // forever (the plugin's install_deb rejects gzip bytes), so it must get // 204 rather than an artifact for a different installer. req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/linux-x86_64-deb/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusNoContent { t.Errorf("status = %d, want 204; body: %s", rr.Code, rr.Body.String()) } } func TestClientUpdate_GitHubError(t *testing.T) { // Server that always returns 500. srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) t.Cleanup(srv.Close) u := updater.NewUpdater("1.0.0", "", "test", "repo") u.SetBaseURL(srv.URL) router := buildClientUpdateRouter(u) req := httptest.NewRequest(http.MethodGet, "/api/v1/client-update/windows-x86_64-nsis/1.0.0", nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) if rr.Code != http.StatusBadGateway { t.Errorf("status = %d, want 502; body: %s", rr.Code, rr.Body.String()) } }