6b85dfce88
For testability, replace most if-based construction logic with calls to well-tested factories living in other packages. While there, acknowledge that a bunch of types could now be private and make them private, modifying the code to call the public factories allowing to construct said types instead. Part of https://github.com/ooni/probe/issues/2121
315 lines
8.0 KiB
Go
315 lines
8.0 KiB
Go
package tracex
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/ooni/probe-cli/v3/internal/model"
|
|
"github.com/ooni/probe-cli/v3/internal/model/mocks"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite"
|
|
"github.com/ooni/probe-cli/v3/internal/netxlite/filtering"
|
|
)
|
|
|
|
func TestMaybeWrapHTTPTransport(t *testing.T) {
|
|
const snapshotSize = 1024
|
|
|
|
t.Run("with non-nil saver", func(t *testing.T) {
|
|
saver := &Saver{}
|
|
underlying := &mocks.HTTPTransport{}
|
|
txp := saver.MaybeWrapHTTPTransport(underlying, snapshotSize)
|
|
realTxp := txp.(*HTTPTransportSaver)
|
|
if realTxp.HTTPTransport != underlying {
|
|
t.Fatal("unexpected result")
|
|
}
|
|
if realTxp.SnapshotSize != snapshotSize {
|
|
t.Fatal("did not set snapshotSize correctly")
|
|
}
|
|
})
|
|
|
|
t.Run("with nil saver", func(t *testing.T) {
|
|
var saver *Saver
|
|
underlying := &mocks.HTTPTransport{}
|
|
txp := saver.MaybeWrapHTTPTransport(underlying, snapshotSize)
|
|
if txp != underlying {
|
|
t.Fatal("unexpected result")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestHTTPTransportSaver(t *testing.T) {
|
|
|
|
t.Run("CloseIdleConnections", func(t *testing.T) {
|
|
var called bool
|
|
child := &mocks.HTTPTransport{
|
|
MockCloseIdleConnections: func() {
|
|
called = true
|
|
},
|
|
}
|
|
dialer := &HTTPTransportSaver{
|
|
HTTPTransport: child,
|
|
Saver: &Saver{},
|
|
}
|
|
dialer.CloseIdleConnections()
|
|
if !called {
|
|
t.Fatal("not called")
|
|
}
|
|
})
|
|
|
|
t.Run("Network", func(t *testing.T) {
|
|
expected := "antani"
|
|
child := &mocks.HTTPTransport{
|
|
MockNetwork: func() string {
|
|
return expected
|
|
},
|
|
}
|
|
dialer := &HTTPTransportSaver{
|
|
HTTPTransport: child,
|
|
Saver: &Saver{},
|
|
}
|
|
if dialer.Network() != expected {
|
|
t.Fatal("unexpected Network")
|
|
}
|
|
})
|
|
|
|
t.Run("RoundTrip", func(t *testing.T) {
|
|
|
|
measureHTTP := func(t *testing.T, URL *url.URL) (*http.Response, *Saver, error) {
|
|
saver := &Saver{}
|
|
txp := &HTTPTransportSaver{
|
|
HTTPTransport: netxlite.NewHTTPTransportStdlib(model.DiscardLogger),
|
|
Saver: saver,
|
|
}
|
|
req, err := http.NewRequest("GET", URL.String(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Add("User-Agent", "miniooni")
|
|
resp, err := txp.RoundTrip(req)
|
|
return resp, saver, err
|
|
}
|
|
|
|
validateRequestFields := func(t *testing.T, value *EventValue, URL *url.URL) {
|
|
if value.HTTPMethod != "GET" {
|
|
t.Fatal("invalid method")
|
|
}
|
|
if value.HTTPRequestHeaders.Get("Host") != URL.Host {
|
|
t.Fatal("invalid Host header")
|
|
}
|
|
if value.HTTPRequestHeaders.Get("User-Agent") != "miniooni" {
|
|
t.Fatal("invalid User-Agent header")
|
|
}
|
|
if value.HTTPURL != URL.String() {
|
|
t.Fatal("invalid URL")
|
|
}
|
|
if value.Time.IsZero() {
|
|
t.Fatal("expected nonzero Time")
|
|
}
|
|
if value.Transport != "tcp" {
|
|
t.Fatal("expected Transport to be tcp")
|
|
}
|
|
}
|
|
|
|
validateRequest := func(t *testing.T, ev Event, URL *url.URL) {
|
|
if _, good := ev.(*EventHTTPTransactionStart); !good {
|
|
t.Fatal("invalid event type")
|
|
}
|
|
if ev.Name() != "http_transaction_start" {
|
|
t.Fatal("invalid event name")
|
|
}
|
|
value := ev.Value()
|
|
validateRequestFields(t, value, URL)
|
|
}
|
|
|
|
validateResponseSuccess := func(t *testing.T, ev Event, URL *url.URL) {
|
|
if _, good := ev.(*EventHTTPTransactionDone); !good {
|
|
t.Fatal("invalid event type")
|
|
}
|
|
if ev.Name() != "http_transaction_done" {
|
|
t.Fatal("invalid event name")
|
|
}
|
|
value := ev.Value()
|
|
validateRequestFields(t, value, URL)
|
|
if value.Duration <= 0 {
|
|
t.Fatal("expected nonzero duration")
|
|
}
|
|
if len(value.HTTPResponseHeaders) <= 0 {
|
|
t.Fatal("expected at least one response header")
|
|
}
|
|
if !bytes.Equal(value.HTTPResponseBody, filtering.HTTPBlockpage451) {
|
|
t.Fatal("unexpected value for response body")
|
|
}
|
|
if value.HTTPStatusCode != 451 {
|
|
t.Fatal("unexpected status code")
|
|
}
|
|
}
|
|
|
|
t.Run("on success", func(t *testing.T) {
|
|
server := filtering.NewHTTPServerCleartext(filtering.HTTPAction451)
|
|
defer server.Close()
|
|
resp, saver, err := measureHTTP(t, server.URL())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 451 {
|
|
t.Fatal("unexpected status code", resp.StatusCode)
|
|
}
|
|
events := saver.Read()
|
|
if len(events) != 2 {
|
|
t.Fatal("unexpected number of events")
|
|
}
|
|
validateRequest(t, events[0], server.URL())
|
|
validateResponseSuccess(t, events[1], server.URL())
|
|
data, err := netxlite.ReadAllContext(context.Background(), resp.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !bytes.Equal(data, filtering.HTTPBlockpage451) {
|
|
t.Fatal("we cannot re-read the same body")
|
|
}
|
|
})
|
|
|
|
validateResponseFailure := func(t *testing.T, ev Event, URL *url.URL) {
|
|
if _, good := ev.(*EventHTTPTransactionDone); !good {
|
|
t.Fatal("invalid event type")
|
|
}
|
|
if ev.Name() != "http_transaction_done" {
|
|
t.Fatal("invalid event name")
|
|
}
|
|
value := ev.Value()
|
|
validateRequestFields(t, value, URL)
|
|
if value.Duration <= 0 {
|
|
t.Fatal("expected nonzero duration")
|
|
}
|
|
if value.Err != netxlite.FailureConnectionReset {
|
|
t.Fatal("unexpected Err value")
|
|
}
|
|
if len(value.HTTPResponseHeaders) > 0 {
|
|
t.Fatal("expected zero response headers")
|
|
}
|
|
if !bytes.Equal(value.HTTPResponseBody, nil) {
|
|
t.Fatal("unexpected value for response body")
|
|
}
|
|
if value.HTTPStatusCode != 0 {
|
|
t.Fatal("unexpected status code")
|
|
}
|
|
}
|
|
|
|
t.Run("on round trip failure", func(t *testing.T) {
|
|
server := filtering.NewHTTPServerCleartext(filtering.HTTPActionReset)
|
|
defer server.Close()
|
|
resp, saver, err := measureHTTP(t, server.URL())
|
|
if err == nil || err.Error() != "connection_reset" {
|
|
t.Fatal("unexpected err", err)
|
|
}
|
|
if resp != nil {
|
|
t.Fatal("expected nil response")
|
|
}
|
|
events := saver.Read()
|
|
if len(events) != 2 {
|
|
t.Fatal("unexpected number of events")
|
|
}
|
|
validateRequest(t, events[0], server.URL())
|
|
validateResponseFailure(t, events[1], server.URL())
|
|
})
|
|
|
|
// Sometimes useful for testing
|
|
/*
|
|
dump := func(t *testing.T, ev Event) {
|
|
data, _ := json.MarshalIndent(ev.Value(), " ", " ")
|
|
t.Log(string(data))
|
|
t.Fail()
|
|
}
|
|
*/
|
|
|
|
t.Run("on error reading the response body", func(t *testing.T) {
|
|
saver := &Saver{}
|
|
expected := errors.New("mocked error")
|
|
txp := HTTPTransportSaver{
|
|
HTTPTransport: &mocks.HTTPTransport{
|
|
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
Header: http.Header{
|
|
"Server": {"antani"},
|
|
},
|
|
StatusCode: 200,
|
|
Body: io.NopCloser(&mocks.Reader{
|
|
MockRead: func(b []byte) (int, error) {
|
|
return 0, expected
|
|
},
|
|
}),
|
|
}, nil
|
|
},
|
|
MockNetwork: func() string {
|
|
return "tcp"
|
|
},
|
|
},
|
|
SnapshotSize: 4,
|
|
Saver: saver,
|
|
}
|
|
URL := &url.URL{
|
|
Scheme: "http",
|
|
Host: "127.0.0.1:9050",
|
|
}
|
|
req, err := http.NewRequest("GET", URL.String(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req.Header.Add("User-Agent", "miniooni")
|
|
resp, err := txp.RoundTrip(req)
|
|
if !errors.Is(err, expected) {
|
|
t.Fatal("not the error we expected")
|
|
}
|
|
if resp != nil {
|
|
t.Fatal("expected nil response")
|
|
}
|
|
ev := saver.Read()
|
|
validateRequest(t, ev[0], URL)
|
|
if ev[1].Value().HTTPStatusCode != 200 {
|
|
t.Fatal("invalid status code")
|
|
}
|
|
if ev[1].Value().HTTPResponseHeaders.Get("Server") != "antani" {
|
|
t.Fatal("invalid Server header")
|
|
}
|
|
if ev[1].Value().Err != "unknown_failure: mocked error" {
|
|
t.Fatal("invalid error")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestHTTPCloneRequestHeaders(t *testing.T) {
|
|
t.Run("with req.Host set", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Host: "www.example.com",
|
|
URL: &url.URL{
|
|
Host: "www.kernel.org",
|
|
},
|
|
Header: http.Header{},
|
|
}
|
|
header := httpCloneRequestHeaders(req)
|
|
if header.Get("Host") != "www.example.com" {
|
|
t.Fatal("did not set Host header correctly")
|
|
}
|
|
})
|
|
|
|
t.Run("with only req.URL.Host set", func(t *testing.T) {
|
|
req := &http.Request{
|
|
Host: "",
|
|
URL: &url.URL{
|
|
Host: "www.kernel.org",
|
|
},
|
|
Header: http.Header{},
|
|
}
|
|
header := httpCloneRequestHeaders(req)
|
|
if header.Get("Host") != "www.kernel.org" {
|
|
t.Fatal("did not set Host header correctly")
|
|
}
|
|
})
|
|
}
|