- cors_profile_test.go: credentials with * refused (global and profile), pattern errors, anchored wildcard origins, Vary: Origin on dynamic answers, api/www credentialed case, unchanged * answers, profile preflight routes and boot errors, turnstile factory params - apidocs_test.go: off by default, pinned 5.33.1 page with the RESEARCH SRI hashes and nonce CSP, ordered compact JSON, provider count, path validation, escaped title - store_lookup_test.go: MemoryStore fallback, published Store, no dial - reorder_hook_test.go (Postgres): hook sees the new order in the tx, its error rolls back, no hook answers unchanged - gate: surf names in the go stage, cabana names in the docker stage
257 lines
7.2 KiB
Go
257 lines
7.2 KiB
Go
package surf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/backpack"
|
|
"git.golem15.com/golem15/summercms/modules/compass"
|
|
"git.golem15.com/golem15/summercms/modules/party"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
)
|
|
|
|
// Smoke tests only; plan 05 of phase 19.3 owns full coverage.
|
|
|
|
const storeHTTPYAML = `body_limits:
|
|
default_bytes: 1048576
|
|
upload_bytes: 1048576
|
|
rate_limit:
|
|
store: redis
|
|
`
|
|
|
|
// redisStoreApp builds an app whose config selects the Redis rate-limit store
|
|
// at redisURL.
|
|
func redisStoreApp(t *testing.T, redisURL string) *backpack.App {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
files := map[string]string{
|
|
"app.yaml": "name: store-test\n",
|
|
"http.yaml": storeHTTPYAML,
|
|
"redis.yaml": fmt.Sprintf("url: %q\n", redisURL),
|
|
}
|
|
for name, body := range files {
|
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{"SUMMER_ENV=development"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return backpack.New(cfg)
|
|
}
|
|
|
|
type countingStore struct{ n atomic.Int64 }
|
|
|
|
func (s *countingStore) Attempt(_ string, max int, decay time.Duration) (bool, int, time.Duration) {
|
|
n := int(s.n.Add(1))
|
|
return n <= max, n, decay
|
|
}
|
|
|
|
func TestBuildRouterStoreSmoke(t *testing.T) {
|
|
// Redis selected but never installed (route:list's path): BuildRouter
|
|
// must not dial and keeps the in-process store.
|
|
app := redisStoreApp(t, "redis://127.0.0.1:1")
|
|
start := time.Now()
|
|
r, err := BuildRouter(app, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d := time.Since(start); d > time.Second {
|
|
t.Fatalf("BuildRouter took %s; it must not dial Redis", d)
|
|
}
|
|
if _, ok := r.limiter.store.(*MemoryStore); !ok {
|
|
t.Fatalf("limiter store = %T, want *MemoryStore", r.limiter.store)
|
|
}
|
|
|
|
// A published Store is what the throttle counts in.
|
|
app = redisStoreApp(t, "redis://127.0.0.1:1")
|
|
fake := &countingStore{}
|
|
if err := app.Publish[Store](fake); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := assemblePlugin{id: "golem15.demo", path: "/ping", use: []string{"throttle:2,1"}}
|
|
h, err := Assemble(app, []party.Plugin{p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/ping", nil))
|
|
if rec.Code != http.StatusOK || fake.n.Load() != 1 {
|
|
t.Fatalf("status %d, fake attempts %d; want 200 through the published store", rec.Code, fake.n.Load())
|
|
}
|
|
}
|
|
|
|
func TestRateLimitAcrossInstancesSmoke(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("requires testcontainers redis")
|
|
}
|
|
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute)
|
|
defer cancel()
|
|
ctr, err := testcontainers.Run(ctx, "redis:7-alpine",
|
|
testcontainers.WithExposedPorts("6379/tcp"),
|
|
testcontainers.WithWaitStrategy(wait.ForListeningPort("6379/tcp")),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("redis: testcontainers: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = testcontainers.TerminateContainer(ctr) })
|
|
host, err := ctr.Host(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port, err := ctr.MappedPort(ctx, "6379/tcp")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
url := fmt.Sprintf("redis://%s:%s/0", host, port.Port())
|
|
|
|
instance := func() http.Handler {
|
|
app := redisStoreApp(t, url)
|
|
hd, err := installCooler(ctx, app)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = hd.Close() })
|
|
p := assemblePlugin{id: "golem15.demo", path: "/ping", use: []string{"throttle:2,1"}}
|
|
h, err := Assemble(app, []party.Plugin{p})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return h
|
|
}
|
|
a, b := instance(), instance()
|
|
get := func(h http.Handler) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
|
|
req.RemoteAddr = "192.0.2.10:4000"
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec
|
|
}
|
|
for i, tc := range []struct {
|
|
h http.Handler
|
|
want int
|
|
}{{a, http.StatusOK}, {b, http.StatusOK}, {a, http.StatusTooManyRequests}} {
|
|
rec := get(tc.h)
|
|
if rec.Code != tc.want {
|
|
t.Fatalf("request %d: status %d, want %d", i+1, rec.Code, tc.want)
|
|
}
|
|
if tc.want == http.StatusTooManyRequests && rec.Header().Get("Retry-After") == "" {
|
|
t.Fatal("429 without Retry-After")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Unit tests (plan 05 of phase 19.3) for BuildRouter's rate-limit store
|
|
// lookup (D-09).
|
|
|
|
func TestBuildRouterStoreFallback(t *testing.T) {
|
|
r, err := BuildRouter(nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := r.limiter.store.(*MemoryStore); !ok {
|
|
t.Fatalf("nil app: store = %T", r.limiter.store)
|
|
}
|
|
app := transportApp(t, "development", map[string]string{"http.yaml": transportBodyLimits})
|
|
r, err = BuildRouter(app, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mem, ok := r.limiter.store.(*MemoryStore)
|
|
if !ok {
|
|
t.Fatalf("no published store: store = %T", r.limiter.store)
|
|
}
|
|
// The fallback store counts: two attempts, max 1.
|
|
if allowed, _, _ := mem.Attempt("k", 1, time.Minute); !allowed {
|
|
t.Fatal("first attempt refused")
|
|
}
|
|
if allowed, _, _ := mem.Attempt("k", 1, time.Minute); allowed {
|
|
t.Fatal("second attempt admitted")
|
|
}
|
|
}
|
|
|
|
func TestBuildRouterUsesPublishedStore(t *testing.T) {
|
|
app := transportApp(t, "development", map[string]string{"http.yaml": transportBodyLimits})
|
|
fake := &countingStore{}
|
|
if err := app.Publish[Store](fake); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r, err := BuildRouter(app, []party.Plugin{assemblePlugin{id: "golem15.demo", path: "/ping", use: []string{"throttle:2,1"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r.limiter.store != Store(fake) {
|
|
t.Fatalf("limiter store = %T, want the published store", r.limiter.store)
|
|
}
|
|
h, err := r.compile()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i, want := range []int{http.StatusOK, http.StatusOK, http.StatusTooManyRequests} {
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/ping", nil))
|
|
if rec.Code != want {
|
|
t.Fatalf("request %d: status %d, want %d", i+1, rec.Code, want)
|
|
}
|
|
}
|
|
if fake.n.Load() != 3 {
|
|
t.Fatalf("published store saw %d attempts, want 3", fake.n.Load())
|
|
}
|
|
}
|
|
|
|
func TestBuildRouterNeverDials(t *testing.T) {
|
|
// A listener standing in for Redis counts every connection.
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var accepted atomic.Int64
|
|
go func() {
|
|
for {
|
|
c, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
accepted.Add(1)
|
|
_ = c.Close()
|
|
}
|
|
}()
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
app := redisStoreApp(t, "redis://"+ln.Addr().String()+"/0")
|
|
start := time.Now()
|
|
r, err := BuildRouter(app, []party.Plugin{assemblePlugin{id: "golem15.demo", path: "/ping", use: []string{"throttle:5,1"}}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h, err := r.compile()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/ping", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d", rec.Code)
|
|
}
|
|
if d := time.Since(start); d > time.Second {
|
|
t.Fatalf("BuildRouter and one request took %s", d)
|
|
}
|
|
if _, ok := r.limiter.store.(*MemoryStore); !ok {
|
|
t.Fatalf("store = %T, want *MemoryStore without install", r.limiter.store)
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
if n := accepted.Load(); n != 0 {
|
|
t.Fatalf("Redis was dialled %d times without installCooler", n)
|
|
}
|
|
}
|