Files
gopeed/pkg/download/extension_git_mirror_test.go
T

211 lines
6.9 KiB
Go

package download
import (
"encoding/json"
"io"
"net/http"
"net/http/cgi"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
httpProtocol "github.com/GopeedLab/gopeed/internal/protocol/http"
"github.com/GopeedLab/gopeed/pkg/base"
)
// Exercise real Git clone/install/check/update without depending on GitHub or
// public mirror availability. The proxy deliberately rejects the official URL.
// github.com/gopeed-test/repo is a placeholder: the successful mirror serves
// the temporary local repository through git http-backend instead.
func newExtensionGitFixture(t *testing.T) (*Downloader, func(string), *atomic.Int32, *atomic.Int32) {
t.Helper()
git, err := exec.LookPath("git")
if err != nil {
t.Skip("Git executable required for HTTP Git fixture")
}
root := t.TempDir()
bare := filepath.Join(root, "repo.git")
work := t.TempDir()
command := func(args ...string) {
t.Helper()
cmd := exec.Command(git, args...)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v: %s", args, err, output)
}
}
command("-c", "init.defaultBranch=main", "init", "--bare", bare)
command("clone", bare, work)
command("-C", work, "config", "user.name", "Gopeed Test")
command("-C", work, "config", "user.email", "[email protected]")
publish := func(version string) {
t.Helper()
directory := filepath.Join(work, "nested", "extension")
if err := os.MkdirAll(directory, 0700); err != nil {
t.Fatal(err)
}
manifest := map[string]any{"name": "mirror-test", "author": "test", "title": "Mirror test", "version": version, "repository": map[string]string{"url": "https://github.com/gopeed-test/repo", "directory": "nested/extension"}}
body, _ := json.Marshal(manifest)
if err := os.WriteFile(filepath.Join(directory, "manifest.json"), body, 0600); err != nil {
t.Fatal(err)
}
command("-C", work, "add", ".")
command("-C", work, "commit", "-m", version)
command("-C", work, "push", "origin", "HEAD:main")
}
publish("1.0.0")
var official, badMirror atomic.Int32
backend := &cgi.Handler{Path: git, Args: []string{"http-backend"}, Env: []string{"GIT_PROJECT_ROOT=" + root, "GIT_HTTP_EXPORT_ALL=1"}}
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, "/https://github.com/gopeed-test")
r.RequestURI = r.URL.RequestURI()
backend.ServeHTTP(w, r)
}))
t.Cleanup(mirror.Close)
failed := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { badMirror.Add(1); http.Error(w, "mirror offline", 502) }))
t.Cleanup(failed.Close)
direct := &http.Transport{}
t.Cleanup(direct.CloseIdleConnections)
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect || r.URL.Hostname() == "github.com" {
official.Add(1)
http.Error(w, "official unavailable", 502)
return
}
request := r.Clone(r.Context())
request.RequestURI = ""
response, err := direct.RoundTrip(request)
if err != nil {
http.Error(w, err.Error(), 502)
return
}
defer response.Body.Close()
for key, values := range response.Header {
for _, value := range values {
w.Header().Add(key, value)
}
}
w.WriteHeader(response.StatusCode)
_, _ = io.Copy(w, response.Body)
}))
t.Cleanup(proxy.Close)
d := testBackendDownloader(t, &httpProtocol.FetcherManager{})
config, _ := d.GetConfig()
config.Proxy = &base.DownloaderProxyConfig{Enable: true, Scheme: "http", Host: strings.TrimPrefix(proxy.URL, "http://")}
config.GithubMirror = &base.GithubMirrorConfig{Enabled: true, Mirrors: []base.GithubMirror{{Type: "ghProxy", URL: failed.URL}, {Type: "ghProxy", URL: mirror.URL}}}
if err := d.PutConfig(config); err != nil {
t.Fatal(err)
}
return d, publish, &official, &badMirror
}
func testInstallExtensionGitURL(t *testing.T, raw string) {
d, _, official, badMirror := newExtensionGitFixture(t)
extension, err := d.InstallExtensionByGit(raw)
if err != nil {
t.Fatal(err)
}
if extension.Version != "1.0.0" || extension.Identity != "test@mirror-test" {
t.Fatalf("%+v", extension)
}
if official.Load() == 0 || badMirror.Load() == 0 {
t.Fatal("ordered fallback was not exercised")
}
if _, err := os.Stat(filepath.Join(d.ExtensionPath(extension), "manifest.json")); err != nil {
t.Fatal(err)
}
}
func TestExtensionGitMirrorInstallAndUpdate(t *testing.T) {
d, publish, official, badMirror := newExtensionGitFixture(t)
extension, err := d.InstallExtensionByGit("https://github.com/gopeed-test/repo#nested/extension")
if err != nil {
t.Fatal(err)
}
publish("1.1.0")
version, err := d.UpgradeCheckExtension(extension.Identity)
if err != nil || version != "1.1.0" {
t.Fatalf("%s %v", version, err)
}
if err = d.UpgradeExtension(extension.Identity); err != nil {
t.Fatal(err)
}
updated, err := d.GetExtension(extension.Identity)
if err != nil || updated.Version != "1.1.0" {
t.Fatalf("%+v %v", updated, err)
}
if updated.Repository.Url != "https://github.com/gopeed-test/repo" {
t.Fatal("mirror URL replaced canonical repository")
}
if official.Load() != 3 || badMirror.Load() != 3 {
t.Fatalf("attempts official=%d mirror=%d", official.Load(), badMirror.Load())
}
}
func TestExtensionGitShutdownWaitsForHandler(t *testing.T) {
d, _, _, _ := newExtensionGitFixture(t)
started := make(chan string, 1)
release := make(chan struct{})
t.Cleanup(func() {
select {
case <-release:
default:
close(release)
}
})
finished := make(chan error, 1)
go func() {
_, err := d.fetchExtensionByGit("https://github.com/gopeed-test/repo#nested/extension", func(path string, _ bool) (*Extension, error) {
started <- path
<-release
return d.parseExtensionByPath(path)
})
finished <- err
}()
var path string
select {
case path = <-started:
case err := <-finished:
t.Fatalf("Git operation finished before handler: %v", err)
case <-time.After(5 * time.Second):
t.Fatal("Git handler did not start")
}
closed := make(chan error, 1)
go func() { closed <- d.Close() }()
select {
case <-d.backgroundCtx.Done():
case <-time.After(5 * time.Second):
t.Fatal("shutdown did not cancel Git context")
}
select {
case err := <-closed:
t.Fatalf("shutdown returned before handler finished: %v", err)
case <-time.After(100 * time.Millisecond):
}
if _, err := os.Stat(filepath.Join(path, "manifest.json")); err != nil {
t.Fatalf("shutdown removed files still used by handler: %v", err)
}
if _, err := d.InstallExtensionByGit("https://github.com/gopeed-test/repo"); err == nil || !strings.Contains(err.Error(), "downloader is closed") {
t.Fatalf("expected new Git operations to be rejected during shutdown: %v", err)
}
close(release)
select {
case err := <-finished:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("Git handler did not finish")
}
select {
case err := <-closed:
if err != nil {
t.Fatal(err)
}
case <-time.After(5 * time.Second):
t.Fatal("shutdown did not finish after Git handler")
}
}