Files
any-hub/tests/integration/upstream_stub_test.go
2025-11-14 12:11:44 +08:00

285 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package integration
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"path"
"sync"
"testing"
"time"
)
type upstreamMode string
const (
upstreamDocker upstreamMode = "docker"
upstreamNPM upstreamMode = "npm"
)
// upstreamStub 暴露简单的 Docker/NPM 上游模拟器,供集成测试复用。
type upstreamStub struct {
server *http.Server
listener net.Listener
URL string
mu sync.Mutex
requests []RecordedRequest
mode upstreamMode
blobBytes []byte
}
// RecordedRequest 捕获每次请求的方法/路径/Host/Headers便于断言代理行为。
type RecordedRequest struct {
Method string
Path string
Host string
Headers http.Header
Body []byte
}
func newUpstreamStub(t *testing.T, mode upstreamMode) *upstreamStub {
t.Helper()
mux := http.NewServeMux()
stub := &upstreamStub{
mode: mode,
blobBytes: []byte("stub-layer-payload"),
}
switch mode {
case upstreamDocker:
registerDockerHandlers(mux, stub.blobBytes)
case upstreamNPM:
registerNPMHandlers(mux)
default:
t.Fatalf("unsupported stub mode: %s", mode)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stub.recordRequest(r)
mux.ServeHTTP(w, r)
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Skipf("unable to start upstream stub listener: %v", err)
}
server := &http.Server{Handler: handler}
stub.server = server
stub.listener = listener
stub.URL = "http://" + listener.Addr().String()
go func() {
_ = server.Serve(listener)
}()
return stub
}
func (s *upstreamStub) Close() {
if s == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if s.server != nil {
_ = s.server.Shutdown(ctx)
}
if s.listener != nil {
_ = s.listener.Close()
}
}
func (s *upstreamStub) recordRequest(r *http.Request) {
body, _ := io.ReadAll(r.Body)
_ = r.Body.Close()
s.mu.Lock()
s.requests = append(s.requests, RecordedRequest{
Method: r.Method,
Path: r.URL.Path,
Host: r.Host,
Headers: cloneHeader(r.Header),
Body: body,
})
s.mu.Unlock()
r.Body = io.NopCloser(bytes.NewReader(body))
}
func (s *upstreamStub) Requests() []RecordedRequest {
s.mu.Lock()
defer s.mu.Unlock()
result := make([]RecordedRequest, len(s.requests))
copy(result, s.requests)
return result
}
func registerDockerHandlers(mux *http.ServeMux, blob []byte) {
mux.HandleFunc("/v2/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v2/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"docker":"ok"}`))
})
mux.HandleFunc("/v2/library/sample/manifests/latest", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.docker.distribution.manifest.v2+json")
resp := map[string]any{
"schemaVersion": 2,
"name": "library/sample",
"tag": "latest",
"layers": []map[string]any{
{
"digest": "sha256:deadbeef",
"size": len(blob),
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
},
},
}
_ = json.NewEncoder(w).Encode(resp)
})
mux.HandleFunc("/v2/library/sample/blobs/", func(w http.ResponseWriter, r *http.Request) {
digest := path.Base(r.URL.Path)
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Docker-Content-Digest", digest)
_, _ = w.Write(blob)
})
}
func registerNPMHandlers(mux *http.ServeMux) {
mux.HandleFunc("/lodash", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{
"name": "lodash",
"dist-tags": map[string]string{
"latest": "4.17.21",
},
"versions": map[string]any{
"4.17.21": map[string]any{
"dist": map[string]any{
"tarball": r.Host + "/lodash/-/lodash-4.17.21.tgz",
},
},
},
}
_ = json.NewEncoder(w).Encode(resp)
})
mux.HandleFunc("/lodash/-/lodash-4.17.21.tgz", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write([]byte("tarball-bytes"))
})
}
func cloneHeader(src http.Header) http.Header {
dst := make(http.Header, len(src))
for k, values := range src {
cp := make([]string, len(values))
copy(cp, values)
dst[k] = cp
}
return dst
}
func TestDockerStubServesManifestAndBlob(t *testing.T) {
stub := newUpstreamStub(t, upstreamDocker)
defer stub.Close()
pingResp, err := http.Get(stub.URL + "/v2/")
if err != nil {
t.Fatalf("docker ping failed: %v", err)
}
pingResp.Body.Close()
if pingResp.StatusCode != http.StatusOK {
t.Fatalf("docker ping unexpected status: %d", pingResp.StatusCode)
}
resp, err := http.Get(stub.URL + "/v2/library/sample/manifests/latest")
if err != nil {
t.Fatalf("manifest request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("unexpected manifest status: %d", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
if !bytes.Contains(body, []byte(`"name":"library/sample"`)) {
t.Fatalf("manifest body unexpected: %s", string(body))
}
layerResp, err := http.Get(stub.URL + "/v2/library/sample/blobs/sha256:deadbeef")
if err != nil {
t.Fatalf("layer request failed: %v", err)
}
defer layerResp.Body.Close()
layer, _ := io.ReadAll(layerResp.Body)
if !bytes.Equal(layer, stub.blobBytes) {
t.Fatalf("layer bytes mismatch: %s", string(layer))
}
if got := len(stub.Requests()); got != 3 {
t.Fatalf("expected 3 recorded requests, got %d", got)
}
}
func TestNPMStubServesMetadataAndTarball(t *testing.T) {
stub := newUpstreamStub(t, upstreamNPM)
defer stub.Close()
resp, err := http.Get(stub.URL + "/lodash")
if err != nil {
t.Fatalf("metadata request failed: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if !bytes.Contains(body, []byte(`"latest":"4.17.21"`)) {
t.Fatalf("metadata unexpected: %s", string(body))
}
tarballResp, err := http.Get(stub.URL + "/lodash/-/lodash-4.17.21.tgz")
if err != nil {
t.Fatalf("tarball request failed: %v", err)
}
defer tarballResp.Body.Close()
data, _ := io.ReadAll(tarballResp.Body)
if !bytes.Equal(data, []byte("tarball-bytes")) {
t.Fatalf("tarball payload mismatch: %s", string(data))
}
if got := len(stub.Requests()); got != 2 {
t.Fatalf("expected 2 recorded requests, got %d", got)
}
}
func TestUpstreamStubSupportsAnonymousCurlHostHeader(t *testing.T) {
stub := newUpstreamStub(t, upstreamDocker)
defer stub.Close()
req, err := http.NewRequest(http.MethodGet, stub.URL+"/v2/", nil)
if err != nil {
t.Fatalf("create request failed: %v", err)
}
req.Host = "docker.hub.local"
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("curl-style request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected 200 from curl-style request, got %d body=%s", resp.StatusCode, string(body))
}
if got := stub.Requests(); len(got) != 1 || got[0].Host != "docker.hub.local" {
t.Fatalf("expected recorded host docker.hub.local, got %v", got)
}
}