krz/skunky-art

Alternative privacy frontend for DeviantArt.

clone: git clone https://gitbay.org/krz/skunky-art.git

v1.3.5: app/httpclient_test.go · raw

  1package app
  2
  3import (
  4	"net/http"
  5	"net/http/httptest"
  6	"sync"
  7	"testing"
  8	"time"
  9)
 10
 11// stubTransport records how many requests reached it and returns an empty 200.
 12type stubTransport struct {
 13	mu sync.Mutex
 14	n  int
 15}
 16
 17func (s *stubTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 18	s.mu.Lock()
 19	s.n++
 20	s.mu.Unlock()
 21	return httptest.NewRecorder().Result(), nil
 22}
 23
 24func newTestThrottle(base http.RoundTripper, gap time.Duration, maxConcurrent int) *daThrottle {
 25	return &daThrottle{base: base, sem: make(chan struct{}, maxConcurrent)}
 26}
 27
 28// DeviantArt requests must be spaced by at least daMinInterval.
 29func TestThrottleRateLimitsDeviantArt(t *testing.T) {
 30	stub := &stubTransport{}
 31	tr := newTestThrottle(stub, daMinInterval, daMaxConcurrent)
 32
 33	start := time.Now()
 34	const n = 3
 35	for range n {
 36		req, _ := http.NewRequest("GET", "https://www.deviantart.com/_puppy/x", nil)
 37		if _, err := tr.RoundTrip(req); err != nil {
 38			t.Fatalf("unexpected error: %v", err)
 39		}
 40	}
 41	elapsed := time.Since(start)
 42
 43	// n requests => at least (n-1) gaps between them.
 44	if want := time.Duration(n-1) * daMinInterval; elapsed < want {
 45		t.Errorf("DA requests were not throttled: %d requests took %v, want >= %v", n, elapsed, want)
 46	}
 47	if stub.n != n {
 48		t.Errorf("expected all %d requests to reach the base transport, got %d", n, stub.n)
 49	}
 50}
 51
 52// Non-DA hosts (e.g. the wixmp image CDN) must not be slowed down.
 53func TestThrottleSkipsOtherHosts(t *testing.T) {
 54	stub := &stubTransport{}
 55	tr := newTestThrottle(stub, daMinInterval, daMaxConcurrent)
 56
 57	start := time.Now()
 58	for range 5 {
 59		req, _ := http.NewRequest("GET", "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/x.jpg", nil)
 60		if _, err := tr.RoundTrip(req); err != nil {
 61			t.Fatalf("unexpected error: %v", err)
 62		}
 63	}
 64
 65	if elapsed := time.Since(start); elapsed >= daMinInterval {
 66		t.Errorf("non-DA host was throttled: 5 requests took %v, want < %v", elapsed, daMinInterval)
 67	}
 68	if stub.n != 5 {
 69		t.Errorf("expected 5 requests through, got %d", stub.n)
 70	}
 71}
 72
 73// Concurrent callers must never exceed daMaxConcurrent in-flight DA requests.
 74func TestThrottleCapsConcurrency(t *testing.T) {
 75	var (
 76		mu       sync.Mutex
 77		inFlight int
 78		peak     int
 79	)
 80	counting := roundTripFunc(func(req *http.Request) (*http.Response, error) {
 81		mu.Lock()
 82		inFlight++
 83		if inFlight > peak {
 84			peak = inFlight
 85		}
 86		mu.Unlock()
 87
 88		time.Sleep(20 * time.Millisecond) // hold the slot
 89
 90		mu.Lock()
 91		inFlight--
 92		mu.Unlock()
 93		return httptest.NewRecorder().Result(), nil
 94	})
 95
 96	tr := newTestThrottle(counting, daMinInterval, daMaxConcurrent)
 97
 98	var wg sync.WaitGroup
 99	for range 6 {
100		wg.Go(func() {
101			req, _ := http.NewRequest("GET", "https://www.deviantart.com/_puppy/x", nil)
102			_, _ = tr.RoundTrip(req)
103		})
104	}
105	wg.Wait()
106
107	if peak > daMaxConcurrent {
108		t.Errorf("concurrency cap breached: peak %d in-flight DA requests, max %d", peak, daMaxConcurrent)
109	}
110}
111
112type roundTripFunc func(*http.Request) (*http.Response, error)
113
114func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
115
116// InstallDAThrottle must preserve proxy-from-environment so HTTPS_PROXY (VPN
117// egress) keeps working, and must not panic on a repeat call.
118func TestInstallDAThrottlePreservesProxy(t *testing.T) {
119	orig := http.DefaultTransport
120	defer func() { http.DefaultTransport = orig }()
121
122	InstallDAThrottle()
123
124	th, ok := http.DefaultTransport.(*daThrottle)
125	if !ok {
126		t.Fatalf("DefaultTransport was not wrapped, got %T", http.DefaultTransport)
127	}
128	base, ok := th.base.(*http.Transport)
129	if !ok {
130		t.Fatalf("base transport is not *http.Transport, got %T", th.base)
131	}
132	if base.Proxy == nil {
133		t.Error("base transport lost its Proxy func: HTTPS_PROXY / VPN egress would break")
134	}
135}