package asnresolve import ( "compress/gzip" "context" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "evobgp/internal/httpclient" ) type ripeRoundTrip func(*http.Request) (*http.Response, error) func (f ripeRoundTrip) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } type ripeCountingBody struct { io.Reader read int closed bool } func (b *ripeCountingBody) Read(p []byte) (int, error) { n, e := b.Reader.Read(p) b.read += n return n, e } func (b *ripeCountingBody) Close() error { b.closed = true; return nil } func TestRIPECompleteResponseLimits(t *testing.T) { cases := []struct { name string limit int prefix string call func(context.Context, *http.Client) error }{ {"announced", 32 << 20, `{"status":"ok","data":{"prefixes":[]}}`, func(ctx context.Context, hc *http.Client) error { _, e := AnnouncedPrefixes(ctx, hc, 64496); return e }}, {"holder", 4 << 20, `{"status":"ok","data":{"holder":"Example"}}`, func(ctx context.Context, hc *http.Client) error { _, e := ASHolderName(ctx, hc, 64496); return e }}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { for _, known := range []bool{true, false} { for _, extra := range []int{0, 1} { body := &ripeCountingBody{Reader: strings.NewReader(tc.prefix + strings.Repeat(" ", tc.limit-len(tc.prefix)+extra))} hc := &http.Client{Transport: ripeRoundTrip(func(r *http.Request) (*http.Response, error) { if _, ok := r.Context().Deadline(); !ok { t.Error("missing request deadline") } length := int64(-1) if known { length = int64(tc.limit + extra) } return &http.Response{StatusCode: 200, Body: body, ContentLength: length, Header: make(http.Header)}, nil })} err := tc.call(context.Background(), hc) if extra == 0 && err != nil { t.Fatalf("exact limit rejected known=%v: %v", known, err) } if extra == 1 && !errors.Is(err, httpclient.ErrResponseTooLarge) { t.Fatalf("oversized valid prefix accepted known=%v: %v", known, err) } if body.read > tc.limit+1 || !body.closed { t.Fatalf("read=%d closed=%v", body.read, body.closed) } } } }) } } func TestRIPEErrorBodySmallAndContextPreserved(t *testing.T) { body := &ripeCountingBody{Reader: strings.NewReader(strings.Repeat("x", 1<<20))} hc := &http.Client{Transport: ripeRoundTrip(func(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 400, Status: "400 Bad Request", Body: body, Header: make(http.Header)}, nil })} if _, err := AnnouncedPrefixes(context.Background(), hc, 1); err == nil || body.read > 4<<10 || !body.closed { t.Fatalf("non200 unbounded read=%d close=%v err=%v", body.read, body.closed, err) } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() hc.Transport = ripeRoundTrip(func(r *http.Request) (*http.Response, error) { <-r.Context().Done(); return nil, r.Context().Err() }) if _, err := ASHolderName(ctx, hc, 1); !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("caller deadline lost: %v", err) } } func TestRIPEGzipExpandedLimit(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(w) _, _ = io.WriteString(gz, `{"status":"ok","data":{"holder":"Example"}}`+strings.Repeat(" ", 4<<20)) _ = gz.Close() })) defer server.Close() t.Setenv("EVOBGP_RIPESTAT_AS_OVERVIEW_URL", server.URL) if _, err := ASHolderName(context.Background(), server.Client(), 1); !errors.Is(err, httpclient.ErrResponseTooLarge) { t.Fatalf("gzip bypassed decoded limit: %v", err) } }