ooni-probe-cli/internal/engine/experiment/simplequicping/simplequicping_test.go

237 lines
5.4 KiB
Go
Raw Permalink Normal View History

package simplequicping
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"log"
"math/big"
"net/url"
"testing"
"time"
"github.com/lucas-clemente/quic-go"
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
"github.com/ooni/probe-cli/v3/internal/model"
)
func TestConfig_alpn(t *testing.T) {
c := Config{}
if c.alpn() != "h3" {
t.Fatal("invalid default alpn list")
}
}
func TestConfig_repetitions(t *testing.T) {
c := Config{}
if c.repetitions() != 10 {
t.Fatal("invalid default number of repetitions")
}
}
func TestConfig_delay(t *testing.T) {
c := Config{}
if c.delay() != time.Second {
t.Fatal("invalid default delay")
}
}
func TestMeasurer_run(t *testing.T) {
// expectedPings is the expected number of pings
const expectedPings = 4
// runHelper is an helper function to run this set of tests.
runHelper := func(input string) (*model.Measurement, model.ExperimentMeasurer, error) {
m := NewExperimentMeasurer(Config{
ALPN: "h3",
Delay: 1, // millisecond
Repetitions: expectedPings,
})
if m.ExperimentName() != "simplequicping" {
t.Fatal("invalid experiment name")
}
if m.ExperimentVersion() != "0.2.0" {
t.Fatal("invalid experiment version")
}
ctx := context.Background()
meas := &model.Measurement{
Input: model.MeasurementTarget(input),
}
sess := &mockable.Session{
MockableLogger: model.DiscardLogger,
}
callbacks := model.NewPrinterCallbacks(model.DiscardLogger)
err := m.Run(ctx, sess, meas, callbacks)
return meas, m, err
}
t.Run("with empty input", func(t *testing.T) {
_, _, err := runHelper("")
if !errors.Is(err, errNoInputProvided) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid URL", func(t *testing.T) {
_, _, err := runHelper("\t")
if !errors.Is(err, errInputIsNotAnURL) {
t.Fatal("unexpected error", err)
}
})
t.Run("with invalid scheme", func(t *testing.T) {
_, _, err := runHelper("https://8.8.8.8:443/")
if !errors.Is(err, errInvalidScheme) {
t.Fatal("unexpected error", err)
}
})
t.Run("with missing port", func(t *testing.T) {
_, _, err := runHelper("quichandshake://8.8.8.8")
if !errors.Is(err, errMissingPort) {
t.Fatal("unexpected error", err)
}
})
t.Run("with local listener", func(t *testing.T) {
srvrURL, listener, err := startEchoServer()
if err != nil {
log.Fatal(err)
}
defer listener.Close()
meas, m, err := runHelper(srvrURL)
if err != nil {
t.Fatal(err)
}
tk := meas.TestKeys.(*TestKeys)
if len(tk.Pings) != expectedPings {
t.Fatal("unexpected number of pings")
}
ask, err := m.GetSummaryKeys(meas)
if err != nil {
t.Fatal("cannot obtain summary")
}
summary := ask.(SummaryKeys)
if summary.IsAnomaly {
t.Fatal("expected no anomaly")
}
})
}
// Start a server that echos all data on the first stream opened by the client.
//
// SPDX-License-Identifier: MIT
//
// See https://github.com/lucas-clemente/quic-go/blob/v0.27.0/example/echo/echo.go#L34
func startEchoServer() (string, quic.Listener, error) {
listener, err := quic.ListenAddr("127.0.0.1:0", generateTLSConfig(), nil)
if err != nil {
return "", nil, err
}
go echoWorkerMain(listener)
URL := &url.URL{
Scheme: "quichandshake",
Host: listener.Addr().String(),
Path: "/",
}
return URL.String(), listener, nil
}
// Worker used by startEchoServer to accept a quic connection.
//
// SPDX-License-Identifier: MIT
//
// See https://github.com/lucas-clemente/quic-go/blob/v0.27.0/example/echo/echo.go#L34
func echoWorkerMain(listener quic.Listener) {
for {
conn, err := listener.Accept(context.Background())
if err != nil {
return
}
stream, err := conn.AcceptStream(context.Background())
if err != nil {
continue
}
stream.Close()
}
}
// Setup a bare-bones TLS config for the server.
//
// SPDX-License-Identifier: MIT
//
// See https://github.com/lucas-clemente/quic-go/blob/v0.27.0/example/echo/echo.go#L91
func generateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"quic-echo-example"},
}
}
func TestConfig_sni(t *testing.T) {
type fields struct {
SNI string
}
type args struct {
address string
}
tests := []struct {
name string
fields fields
args args
want string
}{{
name: "with config.SNI being set",
fields: fields{
SNI: "x.org",
},
args: args{
address: "google.com:443",
},
want: "x.org",
}, {
name: "with invalid endpoint",
fields: fields{},
args: args{
address: "google.com",
},
want: "",
}, {
name: "with valid endpoint",
fields: fields{},
args: args{
address: "google.com:443",
},
want: "google.com",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Config{
SNI: tt.fields.SNI,
}
if got := c.sni(tt.args.address); got != tt.want {
t.Fatalf("Config.sni() = %v, want %v", got, tt.want)
}
})
}
}