mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 08:30:29 +00:00
211 lines
6.9 KiB
Go
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")
|
|
}
|
|
}
|