ooni-probe-cli/internal/netxlite/errwrapper_test.go
Simone Basso c1b06a2d09
fix(netxlite): prefer composition over embedding (#733)
This diff has been extracted and adapted from 8848c8c516

The reason to prefer composition over embedding is that we want the
build to break if we add new methods to interfaces we define. If the build
does not break, we may forget about wrapping methods we should
actually be wrapping. I noticed this issue inside netxlite when I was working
on websteps-illustrated and I added support for NS and PTR queries.

See https://github.com/ooni/probe/issues/2096

While there, perform comprehensive netxlite code review
and apply minor changes and improve the docs.
2022-05-15 19:25:27 +02:00

213 lines
5.7 KiB
Go

package netxlite
import (
"encoding/json"
"errors"
"fmt"
"io"
"testing"
"github.com/ooni/probe-cli/v3/internal/atomicx"
)
func TestErrWrapper(t *testing.T) {
t.Run("Error", func(t *testing.T) {
err := &ErrWrapper{Failure: FailureDNSNXDOMAINError}
if err.Error() != FailureDNSNXDOMAINError {
t.Fatal("invalid return value")
}
})
t.Run("Unwrap", func(t *testing.T) {
err := &ErrWrapper{
Failure: FailureEOFError,
WrappedErr: io.EOF,
}
if !errors.Is(err, io.EOF) {
t.Fatal("cannot unwrap error")
}
})
t.Run("MarshalJSON", func(t *testing.T) {
wrappedErr := &ErrWrapper{
Failure: FailureEOFError,
WrappedErr: io.EOF,
}
data, err := json.Marshal(wrappedErr)
if err != nil {
t.Fatal(err)
}
s := string(data)
if s != "\""+FailureEOFError+"\"" {
t.Fatal("invalid serialization", s)
}
})
}
func TestNewErrWrapper(t *testing.T) {
t.Run("panics if the classifier is nil", func(t *testing.T) {
recovered := &atomicx.Int64{}
func() {
defer func() {
if recover() != nil {
recovered.Add(1)
}
}()
newErrWrapper(nil, CloseOperation, io.EOF)
}()
if recovered.Load() != 1 {
t.Fatal("did not panic")
}
})
t.Run("panics if the operation is empty", func(t *testing.T) {
recovered := &atomicx.Int64{}
func() {
defer func() {
if recover() != nil {
recovered.Add(1)
}
}()
newErrWrapper(classifyGenericError, "", io.EOF)
}()
if recovered.Load() != 1 {
t.Fatal("did not panic")
}
})
t.Run("panics if the error is nil", func(t *testing.T) {
recovered := &atomicx.Int64{}
func() {
defer func() {
if recover() != nil {
recovered.Add(1)
}
}()
newErrWrapper(classifyGenericError, CloseOperation, nil)
}()
if recovered.Load() != 1 {
t.Fatal("did not panic")
}
})
t.Run("otherwise, works as intended", func(t *testing.T) {
ew := newErrWrapper(classifyGenericError, CloseOperation, io.EOF)
if ew.Failure != FailureEOFError {
t.Fatal("unexpected failure")
}
if ew.Operation != CloseOperation {
t.Fatal("unexpected operation")
}
if ew.WrappedErr != io.EOF {
t.Fatal("unexpected WrappedErr")
}
})
t.Run("when the underlying error is already a wrapped error", func(t *testing.T) {
ew := newErrWrapper(classifySyscallError, ReadOperation, ECONNRESET)
var err1 error = ew
err2 := fmt.Errorf("cannot read: %w", err1)
ew2 := newErrWrapper(classifyGenericError, HTTPRoundTripOperation, err2)
if ew2.Failure != ew.Failure {
t.Fatal("not the same failure")
}
if ew2.Operation != HTTPRoundTripOperation {
t.Fatal("not the same operation")
}
if ew2.WrappedErr != err2 {
t.Fatal("invalid underlying error")
}
})
}
func TestNewTopLevelGenericErrWrapper(t *testing.T) {
out := NewTopLevelGenericErrWrapper(io.EOF)
if out.Failure != FailureEOFError {
t.Fatal("invalid failure")
}
if out.Operation != TopLevelOperation {
t.Fatal("invalid operation")
}
if !errors.Is(out, io.EOF) {
t.Fatal("invalid underlying error using errors.Is")
}
if !errors.Is(out.WrappedErr, io.EOF) {
t.Fatal("invalid WrappedErr")
}
}
func TestClassifyOperation(t *testing.T) {
t.Run("for connect", func(t *testing.T) {
// You're doing HTTP and connect fails. You want to know
// that connect failed not that HTTP failed.
err := &ErrWrapper{Operation: ConnectOperation}
if classifyOperation(err, HTTPRoundTripOperation) != ConnectOperation {
t.Fatal("unexpected result")
}
})
t.Run("for http_round_trip", func(t *testing.T) {
// You're doing DoH and something fails inside HTTP. You want
// to know about the internal HTTP error, not resolve.
err := &ErrWrapper{Operation: HTTPRoundTripOperation}
if classifyOperation(err, ResolveOperation) != HTTPRoundTripOperation {
t.Fatal("unexpected result")
}
})
t.Run("for resolve", func(t *testing.T) {
// You're doing HTTP and the DNS fails. You want to
// know that resolve failed.
err := &ErrWrapper{Operation: ResolveOperation}
if classifyOperation(err, HTTPRoundTripOperation) != ResolveOperation {
t.Fatal("unexpected result")
}
})
t.Run("for tls_handshake", func(t *testing.T) {
// You're doing HTTP and the TLS handshake fails. You want
// to know about a TLS handshake error.
err := &ErrWrapper{Operation: TLSHandshakeOperation}
if classifyOperation(err, HTTPRoundTripOperation) != TLSHandshakeOperation {
t.Fatal("unexpected result")
}
})
t.Run("for minor operation", func(t *testing.T) {
// You just noticed that TLS handshake failed and you
// have a child error telling you that read failed. Here
// you want to know about a TLS handshake error.
err := &ErrWrapper{Operation: ReadOperation}
if classifyOperation(err, TLSHandshakeOperation) != TLSHandshakeOperation {
t.Fatal("unexpected result")
}
})
t.Run("for quic_handshake", func(t *testing.T) {
// You're doing HTTP and the QUIC handshake fails. You want
// to know about a QUIC handshake error.
err := &ErrWrapper{Operation: QUICHandshakeOperation}
if classifyOperation(err, HTTPRoundTripOperation) != QUICHandshakeOperation {
t.Fatal("unexpected result")
}
})
t.Run("for quic_handshake_start", func(t *testing.T) {
// You're doing HTTP and the QUIC handshake fails. You want
// to know about a QUIC handshake error.
err := &ErrWrapper{Operation: "quic_handshake_start"}
if classifyOperation(err, HTTPRoundTripOperation) != QUICHandshakeOperation {
t.Fatal("unexpected result")
}
})
t.Run("for quic_handshake_done", func(t *testing.T) {
// You're doing HTTP and the QUIC handshake fails. You want
// to know about a QUIC handshake error.
err := &ErrWrapper{Operation: "quic_handshake_done"}
if classifyOperation(err, HTTPRoundTripOperation) != QUICHandshakeOperation {
t.Fatal("unexpected result")
}
})
}