mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 07:39:19 -05:00
Mirroring pkg:composer failed with unsupported ecosystem. A tagged release is now mirrored with the dist archive upstream.composer lists, under the file name the Composer handler serves it as and checked against the listed shasum. The release's publish time is stored as the handler stores it, so the archive is served from the cache without the metadata, with cooldown on as well. Branches are refused, since the handler reads the metadata for every branch download, and so are archive URLs with a query and packages in upstream.composer_routes. The mirror now finds the whole version rather than its files, for the dist type and publish time. Part of #414.
126 lines
4.5 KiB
Go
126 lines
4.5 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/sha1" //nolint:gosec // Packagist lists SHA-1 shasums
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/git-pkgs/cooldown"
|
|
"github.com/git-pkgs/proxy/internal/config"
|
|
"github.com/git-pkgs/proxy/internal/handler"
|
|
)
|
|
|
|
const composerCommit = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
// composerUpstream serves the Packagist API for acme/widget with release
|
|
// 1.0.0, published in 2020, and branch dev-main, and the 1.0.0 archive.
|
|
type composerUpstream struct {
|
|
*httptest.Server
|
|
archive string
|
|
shasum string
|
|
apiHits atomic.Int32
|
|
distHits atomic.Int32
|
|
}
|
|
|
|
func newComposerUpstream(t *testing.T) *composerUpstream {
|
|
t.Helper()
|
|
u := &composerUpstream{archive: composerZip(`{"name":"acme/widget"}`)}
|
|
sum := sha1.Sum([]byte(u.archive)) //nolint:gosec // see import
|
|
u.shasum = hex.EncodeToString(sum[:])
|
|
u.Server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/packages/acme/widget.json":
|
|
u.apiHits.Add(1)
|
|
dist := func(shasum string) map[string]string {
|
|
return map[string]string{"type": "zip", "url": u.URL + "/zipball/" + composerCommit, "shasum": shasum}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"package": map[string]any{
|
|
"name": "acme/widget",
|
|
"versions": map[string]any{
|
|
"1.0.0": map[string]any{"version": "1.0.0", "time": "2020-01-02T03:04:05+00:00", "dist": dist(u.shasum)},
|
|
"dev-main": map[string]any{"version": "dev-main", "time": "2026-10-01T00:00:00+00:00", "dist": dist("")},
|
|
},
|
|
}})
|
|
case "/zipball/" + composerCommit:
|
|
u.distHits.Add(1)
|
|
_, _ = io.WriteString(w, u.archive)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
}))
|
|
t.Cleanup(u.Close)
|
|
return u
|
|
}
|
|
|
|
func composerMirrorConfig(upstream *composerUpstream) *config.Config {
|
|
cfg := config.Default()
|
|
cfg.Upstream.Composer = upstream.URL
|
|
cfg.Upstream.ComposerRepository = upstream.URL
|
|
return cfg
|
|
}
|
|
|
|
func TestMirrorComposerReleaseIsServedFromCache(t *testing.T) {
|
|
upstream := newComposerUpstream(t)
|
|
cfg := composerMirrorConfig(upstream)
|
|
|
|
progress, proxy := mirrorWithConfig(t, cfg, "pkg:composer/Acme/Widget@1.0.0")
|
|
|
|
if progress.Completed != 1 || upstream.distHits.Load() != 1 {
|
|
t.Fatalf("completed = %d, dist hits = %d, errors = %v", progress.Completed, upstream.distHits.Load(), progress.Errors)
|
|
}
|
|
|
|
// Composer gets the archive from the cache once the upstream is down,
|
|
// with cooldown on as well: the mirror recorded the publish time.
|
|
upstream.Close()
|
|
proxy.Cooldown = &cooldown.Config{Default: "3d"}
|
|
composer := handler.NewComposerHandlerWithUpstreams(proxy, "http://proxy.test", cfg.Upstream.Composer, cfg.Upstream.ComposerRepository)
|
|
rec := httptest.NewRecorder()
|
|
composer.Routes().ServeHTTP(rec, httptest.NewRequest(http.MethodGet,
|
|
"/files/acme/widget/1.0.0/"+composerCommit+".zip", nil))
|
|
if rec.Code != http.StatusOK || rec.Body.String() != upstream.archive {
|
|
t.Errorf("archive from cache: status = %d, body %q; want 200 and the mirrored archive", rec.Code, strings.TrimSpace(rec.Body.String()))
|
|
}
|
|
}
|
|
|
|
func TestMirrorComposerRefusesShasumMismatch(t *testing.T) {
|
|
upstream := newComposerUpstream(t)
|
|
upstream.shasum = strings.Repeat("0", 40)
|
|
|
|
progress, _ := mirrorWithConfig(t, composerMirrorConfig(upstream), "pkg:composer/acme/widget@1.0.0")
|
|
|
|
if progress.Failed != 1 || progress.Completed != 0 {
|
|
t.Errorf("completed = %d, failed = %d, errors = %v; want the archive refused", progress.Completed, progress.Failed, progress.Errors)
|
|
}
|
|
}
|
|
|
|
func TestMirrorComposerRefusesBranches(t *testing.T) {
|
|
upstream := newComposerUpstream(t)
|
|
|
|
progress, _ := mirrorWithConfig(t, composerMirrorConfig(upstream), "pkg:composer/acme/widget@dev-main")
|
|
|
|
if progress.Failed != 1 || len(progress.Errors) != 1 || !strings.Contains(progress.Errors[0].Error, "branch") {
|
|
t.Errorf("failed = %d, errors = %v; want the branch refused", progress.Failed, progress.Errors)
|
|
}
|
|
if upstream.distHits.Load() != 0 {
|
|
t.Error("the branch archive was fetched")
|
|
}
|
|
}
|
|
|
|
func TestMirrorComposerRefusesRoutedPackages(t *testing.T) {
|
|
upstream := newComposerUpstream(t)
|
|
cfg := composerMirrorConfig(upstream)
|
|
cfg.Upstream.ComposerRoutes = map[string]string{"acme/*": "https://composer.example.com"}
|
|
|
|
progress, _ := mirrorWithConfig(t, cfg, "pkg:composer/acme/widget@1.0.0")
|
|
|
|
if progress.Failed != 1 || upstream.apiHits.Load() != 0 || upstream.distHits.Load() != 0 {
|
|
t.Errorf("failed = %d, api hits = %d, dist hits = %d; want a routed package refused without asking Packagist",
|
|
progress.Failed, upstream.apiHits.Load(), upstream.distHits.Load())
|
|
}
|
|
}
|