Files

296 lines
8.8 KiB
Go

package source
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
)
func TestURLs(t *testing.T) {
t.Setenv("MAGPIE_MIRRORS", "on")
t.Setenv("MAGPIE_NPM_REGISTRY", "")
t.Setenv("MAGPIE_GITHUB_MIRROR", "")
got := URLs("https://registry.npmjs.org/@scope/pkg/latest?x=1")
want := []string{
"https://registry.npmjs.org/@scope/pkg/latest?x=1",
"https://registry.npmmirror.com/@scope/pkg/latest?x=1",
}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Fatalf("npm URLs = %q, want %q", got, want)
}
got = URLs("https://github.com/owner/repo/releases/download/v1/file")
want = []string{
"https://github.com/owner/repo/releases/download/v1/file",
"https://gh-proxy.com/https://github.com/owner/repo/releases/download/v1/file",
}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Fatalf("github URLs = %q, want %q", got, want)
}
t.Setenv("MAGPIE_NPM_REGISTRY", "https://npm.example/")
t.Setenv("MAGPIE_GITHUB_MIRROR", "https://gh.example")
got = URLs("https://registry.npmjs.org/pkg")
if got[1] != "https://npm.example/pkg" {
t.Fatalf("npm mirror = %q", got[1])
}
got = URLs("https://api.github.com/repos/o/r")
if got[1] != "https://gh.example/https://api.github.com/repos/o/r" {
t.Fatalf("github mirror = %q", got[1])
}
}
func TestURLsDefaultOff(t *testing.T) {
t.Setenv("MAGPIE_MIRRORS", "")
got := URLs("https://github.com/o/r")
if len(got) != 1 || got[0] != "https://github.com/o/r" {
t.Fatalf("URLs by default = %q", got)
}
}
func TestURLsOff(t *testing.T) {
t.Setenv("MAGPIE_MIRRORS", "off")
got := URLs("https://github.com/o/r")
if len(got) != 1 || got[0] != "https://github.com/o/r" {
t.Fatalf("URLs with mirrors off = %q", got)
}
}
func TestDoFallsBack(t *testing.T) {
var mirrorHits atomic.Int32
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "down", http.StatusBadGateway)
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mirrorHits.Add(1)
io.WriteString(w, "from mirror")
}))
defer mirror.Close()
req, _ := http.NewRequest("GET", official.URL+"/thing", nil)
resp, err := do(http.DefaultClient, req, []string{official.URL + "/thing", mirror.URL + "/thing"})
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if string(body) != "from mirror" || mirrorHits.Load() != 1 {
t.Fatalf("body %q, mirror hits %d", body, mirrorHits.Load())
}
}
func TestDoFallsBackOnRateLimit(t *testing.T) {
var mirrorHits atomic.Int32
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-RateLimit-Remaining", "0")
http.Error(w, "limited", http.StatusForbidden)
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mirrorHits.Add(1)
io.WriteString(w, "from mirror")
}))
defer mirror.Close()
req, _ := http.NewRequest("GET", official.URL+"/thing", nil)
resp, err := do(http.DefaultClient, req, []string{official.URL + "/thing", mirror.URL + "/thing"})
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if mirrorHits.Load() != 1 {
t.Fatalf("mirror hits %d", mirrorHits.Load())
}
}
func TestCredentialsStayOfficial(t *testing.T) {
req, _ := http.NewRequest("GET", "https://api.github.com/repos/o/r", nil)
if !mirrorable(req) {
t.Fatal("a request without credentials can't use a mirror")
}
req.Header.Set("Authorization", "Bearer secret")
if mirrorable(req) {
t.Fatal("a request with Authorization can use a mirror")
}
req.Header.Del("Authorization")
req.Header.Set("Cookie", "session=secret")
if mirrorable(req) {
t.Fatal("a request with a cookie can use a mirror")
}
req.Header.Del("Cookie")
req.URL.User = url.UserPassword("user", "secret")
if mirrorable(req) {
t.Fatal("a request with URL userinfo can use a mirror")
}
req.URL.User = nil
q := req.URL.Query()
q.Set("access_token", "secret")
req.URL.RawQuery = q.Encode()
if mirrorable(req) {
t.Fatal("a request with an access_token can use a mirror")
}
}
func TestDoOfficialSkipsMirror(t *testing.T) {
var mirrorHits atomic.Int32
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "official")
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mirrorHits.Add(1)
io.WriteString(w, "mirror")
}))
defer mirror.Close()
t.Setenv("MAGPIE_GITHUB_MIRROR", mirror.URL)
req, _ := http.NewRequest("GET", official.URL+"/thing", nil)
resp, err := DoOfficial(http.DefaultClient, req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if mirrorHits.Load() != 0 {
t.Fatalf("mirror hits %d", mirrorHits.Load())
}
}
func TestDoJoinsErrors(t *testing.T) {
official := "http://127.0.0.1:1/thing"
mirror := "http://127.0.0.1:2/thing"
req, _ := http.NewRequest("GET", official, nil)
_, err := do(http.DefaultClient, req, []string{official, mirror})
if err == nil {
t.Fatal("both sources failed but no error")
}
if !strings.Contains(err.Error(), official) || !strings.Contains(err.Error(), mirror) {
t.Fatalf("error %q doesn't name both sources", err)
}
}
func TestDoKeepsAnAnswer(t *testing.T) {
var mirrorHits atomic.Int32
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mirrorHits.Add(1)
}))
defer mirror.Close()
req, _ := http.NewRequest("GET", official.URL+"/thing", nil)
resp, err := do(http.DefaultClient, req, []string{official.URL + "/thing", mirror.URL + "/thing"})
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNotFound || mirrorHits.Load() != 0 {
t.Fatalf("status %d, mirror hits %d", resp.StatusCode, mirrorHits.Load())
}
}
func TestDoBodyOutlivesCandidateTimeout(t *testing.T) {
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "down", http.StatusBadGateway)
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
time.Sleep(1200 * time.Millisecond)
io.WriteString(w, "slow mirror")
}))
defer mirror.Close()
req, _ := http.NewRequest("GET", official.URL+"/thing", nil)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := do(client, req, []string{official.URL + "/thing", mirror.URL + "/thing"})
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != "slow mirror" {
t.Fatalf("body = %q", body)
}
}
type recordedTransport struct {
requests []string
}
func (rt *recordedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
rt.requests = append(rt.requests, req.URL.String())
status := http.StatusBadGateway
if strings.HasPrefix(req.URL.String(), "https://gh.example/") {
status = http.StatusOK
}
return &http.Response{
StatusCode: status,
Status: fmt.Sprintf("%d %s", status, http.StatusText(status)),
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("")),
Request: req,
}, nil
}
func TestDoKeepsCredentialsOfficial(t *testing.T) {
t.Setenv("MAGPIE_MIRRORS", "on")
t.Setenv("MAGPIE_GITHUB_MIRROR", "https://gh.example")
rt := &recordedTransport{}
req, _ := http.NewRequest("GET", "https://api.github.com/repos/o/r", nil)
req.Header.Set("Authorization", "Bearer secret")
resp, err := Do(&http.Client{Transport: rt}, req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()
if len(rt.requests) != 1 || rt.requests[0] != "https://api.github.com/repos/o/r" {
t.Fatalf("requests = %q", rt.requests)
}
}
func TestDoLastSourceUsesRemainingBudget(t *testing.T) {
official := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(1100 * time.Millisecond)
http.Error(w, "slow official", http.StatusBadGateway)
}))
defer official.Close()
mirror := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(700 * time.Millisecond)
io.WriteString(w, "slow mirror")
}))
defer mirror.Close()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", official.URL+"/thing", nil)
resp, err := do(http.DefaultClient, req, []string{official.URL + "/thing", mirror.URL + "/thing"})
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if string(body) != "slow mirror" {
t.Fatalf("body = %q", body)
}
}