refactor(tracex): convert to unit testing (#781)

The exercise already allowed me to notice issues such as fields not
being properly initialized by savers.

This is one of the last steps before moving tracex away from the
internal/netx package and into the internal package.

See https://github.com/ooni/probe/issues/2121
This commit is contained in:
Simone Basso 2022-06-01 23:15:47 +02:00 committed by GitHub
commit d397036073
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1674 additions and 1111 deletions

View file

@ -3,291 +3,250 @@ package tracex
import (
"context"
"crypto/tls"
"reflect"
"crypto/x509"
"errors"
"net"
"testing"
"time"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
)
func TestSaverTLSHandshakerSuccessWithReadWrite(t *testing.T) {
// This is the most common use case for collecting reads, writes
if testing.Short() {
t.Skip("skip test in short mode")
}
nextprotos := []string{"h2"}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Config: &tls.Config{NextProtos: nextprotos},
Dialer: netxlite.NewDialerWithResolver(
model.DiscardLogger,
netxlite.NewResolverStdlib(model.DiscardLogger),
saver.NewReadWriteObserver(),
),
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
// Implementation note: we don't close the connection here because it is
// very handy to have the last event being the end of the handshake
_, err := tlsdlr.DialTLSContext(context.Background(), "tcp", "www.google.com:443")
if err != nil {
t.Fatal(err)
}
ev := saver.Read()
if len(ev) < 4 {
// it's a bit tricky to be sure about the right number of
// events because network conditions may influence that
t.Fatal("unexpected number of events")
}
if ev[0].Name() != "tls_handshake_start" {
t.Fatal("unexpected Name")
}
if ev[0].Value().TLSServerName != "www.google.com" {
t.Fatal("unexpected TLSServerName")
}
if !reflect.DeepEqual(ev[0].Value().TLSNextProtos, nextprotos) {
t.Fatal("unexpected TLSNextProtos")
}
if ev[0].Value().Time.After(time.Now()) {
t.Fatal("unexpected Time")
}
last := len(ev) - 1
for idx := 1; idx < last; idx++ {
if ev[idx].Value().Data == nil {
t.Fatal("unexpected Data")
func TestTLSHandshakerSaver(t *testing.T) {
t.Run("Handshake", func(t *testing.T) {
checkStartEventFields := func(t *testing.T, value *EventValue) {
if value.Address != "8.8.8.8:443" {
t.Fatal("invalid Address")
}
if !value.NoTLSVerify {
t.Fatal("expected NoTLSVerify to be true")
}
if value.Proto != "tcp" {
t.Fatal("wrong protocol")
}
if diff := cmp.Diff(value.TLSNextProtos, []string{"h2"}); diff != "" {
t.Fatal(diff)
}
if value.TLSServerName != "dns.google" {
t.Fatal("invalid TLSServerName")
}
if value.Time.IsZero() {
t.Fatal("expected non zero time")
}
}
if ev[idx].Value().Duration <= 0 {
t.Fatal("unexpected Duration")
checkStartedEvent := func(t *testing.T, ev Event) {
if _, good := ev.(*EventTLSHandshakeStart); !good {
t.Fatal("invalid event type")
}
value := ev.Value()
checkStartEventFields(t, value)
}
if ev[idx].Value().Err != nil {
t.Fatal("unexpected Err")
checkDoneEventFieldsSuccess := func(t *testing.T, value *EventValue) {
if value.Duration <= 0 {
t.Fatal("expected non-zero duration")
}
if value.Err != nil {
t.Fatal("expected no error here")
}
if value.TLSCipherSuite != "TLS_RSA_WITH_RC4_128_SHA" {
t.Fatal("invalid cipher suite")
}
if value.TLSNegotiatedProto != "h2" {
t.Fatal("invalid negotiated protocol")
}
if diff := cmp.Diff(value.TLSPeerCerts, []*x509.Certificate{}); diff != "" {
t.Fatal(diff)
}
if value.TLSVersion != "TLSv1.3" {
t.Fatal("invalid TLS version")
}
}
if ev[idx].Value().NumBytes <= 0 {
t.Fatal("unexpected NumBytes")
checkDoneEvent := func(t *testing.T, ev Event, fun func(t *testing.T, value *EventValue)) {
if _, good := ev.(*EventTLSHandshakeDone); !good {
t.Fatal("invalid event type")
}
value := ev.Value()
checkStartEventFields(t, value)
fun(t, value)
}
switch ev[idx].Name() {
case netxlite.ReadOperation, netxlite.WriteOperation:
default:
t.Fatal("unexpected Name")
t.Run("on success", func(t *testing.T) {
saver := &Saver{}
returnedConnState := tls.ConnectionState{
CipherSuite: tls.TLS_RSA_WITH_RC4_128_SHA,
NegotiatedProtocol: "h2",
PeerCertificates: []*x509.Certificate{},
Version: tls.VersionTLS13,
}
returnedConn := &mocks.TLSConn{
MockConnectionState: func() tls.ConnectionState {
return returnedConnState
},
}
thx := saver.WrapTLSHandshaker(&mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn,
config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return returnedConn, returnedConnState, nil
},
})
ctx := context.Background()
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"h2"},
ServerName: "dns.google",
}
tcpConn := &mocks.Conn{
MockRemoteAddr: func() net.Addr {
return &mocks.Addr{
MockString: func() string {
return "8.8.8.8:443"
},
MockNetwork: func() string {
return "tcp"
},
}
},
}
conn, _, err := thx.Handshake(ctx, tcpConn, tlsConfig)
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("expected non-nil conn")
}
events := saver.Read()
if len(events) != 2 {
t.Fatal("expected two events")
}
checkStartedEvent(t, events[0])
checkDoneEvent(t, events[1], checkDoneEventFieldsSuccess)
})
checkDoneEventFieldsFailure := func(t *testing.T, value *EventValue) {
if value.Duration <= 0 {
t.Fatal("expected non-zero duration")
}
if value.Err == nil {
t.Fatal("expected non-nil error here")
}
if value.TLSCipherSuite != "" {
t.Fatal("invalid TLS cipher suite")
}
if value.TLSNegotiatedProto != "" {
t.Fatal("invalid negotiated proto")
}
if len(value.TLSPeerCerts) > 0 {
t.Fatal("expected no peer certs")
}
if value.TLSVersion != "" {
t.Fatal("invalid TLS version")
}
}
if ev[idx].Value().Time.Before(ev[idx-1].Value().Time) {
t.Fatal("unexpected Time")
}
}
if ev[last].Value().Duration <= 0 {
t.Fatal("unexpected Duration")
}
if ev[last].Value().Err != nil {
t.Fatal("unexpected Err")
}
if ev[last].Name() != "tls_handshake_done" {
t.Fatal("unexpected Name")
}
if ev[last].Value().TLSCipherSuite == "" {
t.Fatal("unexpected TLSCipherSuite")
}
if ev[last].Value().TLSNegotiatedProto != "h2" {
t.Fatal("unexpected TLSNegotiatedProto")
}
if !reflect.DeepEqual(ev[last].Value().TLSNextProtos, nextprotos) {
t.Fatal("unexpected TLSNextProtos")
}
if ev[last].Value().TLSPeerCerts == nil {
t.Fatal("unexpected TLSPeerCerts")
}
if ev[last].Value().TLSServerName != "www.google.com" {
t.Fatal("unexpected TLSServerName")
}
if ev[last].Value().TLSVersion == "" {
t.Fatal("unexpected TLSVersion")
}
if ev[last].Value().Time.Before(ev[last-1].Value().Time) {
t.Fatal("unexpected Time")
}
t.Run("on failure", func(t *testing.T) {
expected := errors.New("mocked error")
saver := &Saver{}
thx := saver.WrapTLSHandshaker(&mocks.TLSHandshaker{
MockHandshake: func(ctx context.Context, conn net.Conn,
config *tls.Config) (net.Conn, tls.ConnectionState, error) {
return nil, tls.ConnectionState{}, expected
},
})
ctx := context.Background()
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"h2"},
ServerName: "dns.google",
}
tcpConn := &mocks.Conn{
MockRemoteAddr: func() net.Addr {
return &mocks.Addr{
MockString: func() string {
return "8.8.8.8:443"
},
MockNetwork: func() string {
return "tcp"
},
}
},
}
conn, _, err := thx.Handshake(ctx, tcpConn, tlsConfig)
if !errors.Is(err, expected) {
t.Fatal("unexpected err", err)
}
if conn != nil {
t.Fatal("expected nil conn")
}
events := saver.Read()
if len(events) != 2 {
t.Fatal("expected two events")
}
checkStartedEvent(t, events[0])
checkDoneEvent(t, events[1], checkDoneEventFieldsFailure)
})
})
}
func TestSaverTLSHandshakerSuccess(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
func Test_tlsPeerCerts(t *testing.T) {
cert0 := &x509.Certificate{Raw: []byte{1, 2, 3, 4}}
type args struct {
state tls.ConnectionState
err error
}
nextprotos := []string{"h2"}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Config: &tls.Config{NextProtos: nextprotos},
Dialer: &netxlite.DialerSystem{},
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
conn, err := tlsdlr.DialTLSContext(context.Background(), "tcp", "www.google.com:443")
if err != nil {
t.Fatal(err)
}
conn.Close()
ev := saver.Read()
if len(ev) != 2 {
t.Fatal("unexpected number of events")
}
if ev[0].Name() != "tls_handshake_start" {
t.Fatal("unexpected Name")
}
if ev[0].Value().TLSServerName != "www.google.com" {
t.Fatal("unexpected TLSServerName")
}
if !reflect.DeepEqual(ev[0].Value().TLSNextProtos, nextprotos) {
t.Fatal("unexpected TLSNextProtos")
}
if ev[0].Value().Time.After(time.Now()) {
t.Fatal("unexpected Time")
}
if ev[1].Value().Duration <= 0 {
t.Fatal("unexpected Duration")
}
if ev[1].Value().Err != nil {
t.Fatal("unexpected Err")
}
if ev[1].Name() != "tls_handshake_done" {
t.Fatal("unexpected Name")
}
if ev[1].Value().TLSCipherSuite == "" {
t.Fatal("unexpected TLSCipherSuite")
}
if ev[1].Value().TLSNegotiatedProto != "h2" {
t.Fatal("unexpected TLSNegotiatedProto")
}
if !reflect.DeepEqual(ev[1].Value().TLSNextProtos, nextprotos) {
t.Fatal("unexpected TLSNextProtos")
}
if ev[1].Value().TLSPeerCerts == nil {
t.Fatal("unexpected TLSPeerCerts")
}
if ev[1].Value().TLSServerName != "www.google.com" {
t.Fatal("unexpected TLSServerName")
}
if ev[1].Value().TLSVersion == "" {
t.Fatal("unexpected TLSVersion")
}
if ev[1].Value().Time.Before(ev[0].Value().Time) {
t.Fatal("unexpected Time")
}
}
func TestSaverTLSHandshakerHostnameError(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Dialer: &netxlite.DialerSystem{},
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
conn, err := tlsdlr.DialTLSContext(
context.Background(), "tcp", "wrong.host.badssl.com:443")
if err == nil {
t.Fatal("expected an error here")
}
if conn != nil {
t.Fatal("expected nil conn here")
}
for _, ev := range saver.Read() {
if ev.Name() != "tls_handshake_done" {
continue
}
if ev.Value().NoTLSVerify == true {
t.Fatal("expected NoTLSVerify to be false")
}
if len(ev.Value().TLSPeerCerts) < 1 {
t.Fatal("expected at least a certificate here")
}
}
}
func TestSaverTLSHandshakerInvalidCertError(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Dialer: &netxlite.DialerSystem{},
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
conn, err := tlsdlr.DialTLSContext(
context.Background(), "tcp", "expired.badssl.com:443")
if err == nil {
t.Fatal("expected an error here")
}
if conn != nil {
t.Fatal("expected nil conn here")
}
for _, ev := range saver.Read() {
if ev.Name() != "tls_handshake_done" {
continue
}
if ev.Value().NoTLSVerify == true {
t.Fatal("expected NoTLSVerify to be false")
}
if len(ev.Value().TLSPeerCerts) < 1 {
t.Fatal("expected at least a certificate here")
}
}
}
func TestSaverTLSHandshakerAuthorityError(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Dialer: &netxlite.DialerSystem{},
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
conn, err := tlsdlr.DialTLSContext(
context.Background(), "tcp", "self-signed.badssl.com:443")
if err == nil {
t.Fatal("expected an error here")
}
if conn != nil {
t.Fatal("expected nil conn here")
}
for _, ev := range saver.Read() {
if ev.Name() != "tls_handshake_done" {
continue
}
if ev.Value().NoTLSVerify == true {
t.Fatal("expected NoTLSVerify to be false")
}
if len(ev.Value().TLSPeerCerts) < 1 {
t.Fatal("expected at least a certificate here")
}
}
}
func TestSaverTLSHandshakerNoTLSVerify(t *testing.T) {
if testing.Short() {
t.Skip("skip test in short mode")
}
saver := &Saver{}
tlsdlr := &netxlite.TLSDialerLegacy{
Config: &tls.Config{InsecureSkipVerify: true},
Dialer: &netxlite.DialerSystem{},
TLSHandshaker: saver.WrapTLSHandshaker(&netxlite.TLSHandshakerConfigurable{}),
}
conn, err := tlsdlr.DialTLSContext(
context.Background(), "tcp", "self-signed.badssl.com:443")
if err != nil {
t.Fatal(err)
}
if conn == nil {
t.Fatal("expected non-nil conn here")
}
conn.Close()
for _, ev := range saver.Read() {
if ev.Name() != "tls_handshake_done" {
continue
}
if ev.Value().NoTLSVerify != true {
t.Fatal("expected NoTLSVerify to be true")
}
if len(ev.Value().TLSPeerCerts) < 1 {
t.Fatal("expected at least a certificate here")
}
tests := []struct {
name string
args args
want []*x509.Certificate
}{{
name: "no error",
args: args{
state: tls.ConnectionState{
PeerCertificates: []*x509.Certificate{cert0},
},
},
want: []*x509.Certificate{cert0},
}, {
name: "all empty",
args: args{},
want: nil,
}, {
name: "x509.HostnameError",
args: args{
state: tls.ConnectionState{},
err: x509.HostnameError{
Certificate: cert0,
},
},
want: []*x509.Certificate{cert0},
}, {
name: "x509.UnknownAuthorityError",
args: args{
state: tls.ConnectionState{},
err: x509.UnknownAuthorityError{
Cert: cert0,
},
},
want: []*x509.Certificate{cert0},
}, {
name: "x509.CertificateInvalidError",
args: args{
state: tls.ConnectionState{},
err: x509.CertificateInvalidError{
Cert: cert0,
},
},
want: []*x509.Certificate{cert0},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tlsPeerCerts(tt.args.state, tt.args.err)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatal(diff)
}
})
}
}