refactor: interfaces and data types into the model package (#642)
## Checklist - [x] I have read the [contribution guidelines](https://github.com/ooni/probe-cli/blob/master/CONTRIBUTING.md) - [x] reference issue for this pull request: https://github.com/ooni/probe/issues/1885 - [x] related ooni/spec pull request: N/A Location of the issue tracker: https://github.com/ooni/probe ## Description This PR contains a set of changes to move important interfaces and data types into the `./internal/model` package. The criteria for including an interface or data type in here is roughly that the type should be important and used by several packages. We are especially interested to move more interfaces here to increase modularity. An additional side effect is that, by reading this package, one should be able to understand more quickly how different parts of the codebase interact with each other. This is what I want to move in `internal/model`: - [x] most important interfaces from `internal/netxlite` - [x] everything that was previously part of `internal/engine/model` - [x] mocks from `internal/netxlite/mocks` should also be moved in here as a subpackage
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Package github.com/ooni/probe-engine/model
|
||||
|
||||
Shared data structures and interfaces. We include in this
|
||||
package the most fundamental types. Use `go doc` to get
|
||||
more thorough documentation about what is inside this package
|
||||
and when to put a type inside this package.
|
||||
@@ -0,0 +1,40 @@
|
||||
// Package model contains the shared interfaces and data structures.
|
||||
//
|
||||
// Criteria for adding a type to this package
|
||||
//
|
||||
// This package should contain two types:
|
||||
//
|
||||
// 1. important interfaces that are shared by several packages
|
||||
// within the codebase, with the objective of separating unrelated
|
||||
// pieces of code and making unit testing easier;
|
||||
//
|
||||
// 2. important pieces of data that are shared across different
|
||||
// packages (e.g., the representation of a Measurement).
|
||||
//
|
||||
// In general, this package should not contain logic, unless
|
||||
// this logic is strictly related to data structures and we
|
||||
// cannot implement this logic elsewhere.
|
||||
//
|
||||
// Content of this package
|
||||
//
|
||||
// The following list (which may not always be up-to-date)
|
||||
// summarizes the categories of types that currently belong here
|
||||
// and names the files in which they are implemented:
|
||||
//
|
||||
// - experiment.go: generic definition of a network experiment
|
||||
// and all the required support types;
|
||||
//
|
||||
// - keyvaluestore.go: generic definition of a key-value store,
|
||||
// used in several places across the codebase;
|
||||
//
|
||||
// - logger.go: generic definition of an apex/log compatible logger,
|
||||
// used in several places across the codebase;
|
||||
//
|
||||
// - measurement.go: data type representing the result of
|
||||
// a network measurement, used in many many places;
|
||||
//
|
||||
// - netx.go: network extension interfaces and data used everywhere
|
||||
// we need to perform network operations;
|
||||
//
|
||||
// - ooapi.go: types to communicate with the OONI API.
|
||||
package model
|
||||
@@ -0,0 +1,117 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//
|
||||
// Definition of experiment and types used by the
|
||||
// implemenation of all experiments.
|
||||
//
|
||||
|
||||
// ExperimentSession is the experiment's view of a session.
|
||||
type ExperimentSession interface {
|
||||
GetTestHelpersByName(name string) ([]OOAPIService, bool)
|
||||
DefaultHTTPClient() *http.Client
|
||||
FetchPsiphonConfig(ctx context.Context) ([]byte, error)
|
||||
FetchTorTargets(ctx context.Context, cc string) (map[string]OOAPITorTarget, error)
|
||||
FetchURLList(ctx context.Context, config OOAPIURLListConfig) ([]OOAPIURLInfo, error)
|
||||
Logger() Logger
|
||||
ProbeCC() string
|
||||
ResolverIP() string
|
||||
TempDir() string
|
||||
TorArgs() []string
|
||||
TorBinary() string
|
||||
UserAgent() string
|
||||
}
|
||||
|
||||
// ExperimentAsyncTestKeys is the type of test keys returned by an experiment
|
||||
// when running in async fashion rather than in sync fashion.
|
||||
type ExperimentAsyncTestKeys struct {
|
||||
// Extensions contains the extensions used by this experiment.
|
||||
Extensions map[string]int64
|
||||
|
||||
// Input is the input this measurement refers to.
|
||||
Input MeasurementTarget
|
||||
|
||||
// MeasurementRuntime is the total measurement runtime.
|
||||
MeasurementRuntime float64
|
||||
|
||||
// TestKeys contains the actual test keys.
|
||||
TestKeys interface{}
|
||||
}
|
||||
|
||||
// ExperimentMeasurerAsync is a measurer that can run in async fashion.
|
||||
//
|
||||
// Currently this functionality is optional, but we will likely
|
||||
// migrate all experiments to use this functionality in 2022.
|
||||
type ExperimentMeasurerAsync interface {
|
||||
// RunAsync runs the experiment in async fashion.
|
||||
//
|
||||
// Arguments:
|
||||
//
|
||||
// - ctx is the context for deadline/timeout/cancellation
|
||||
//
|
||||
// - sess is the measurement session
|
||||
//
|
||||
// - input is the input URL to measure
|
||||
//
|
||||
// - callbacks contains the experiment callbacks
|
||||
//
|
||||
// Returns either a channel where TestKeys are posted or an error.
|
||||
//
|
||||
// An error indicates that specific preconditions for running the experiment
|
||||
// are not met (e.g., the input URL is invalid).
|
||||
//
|
||||
// On success, the experiment will post on the channel each new
|
||||
// measurement until it is done and closes the channel.
|
||||
RunAsync(ctx context.Context, sess ExperimentSession, input string,
|
||||
callbacks ExperimentCallbacks) (<-chan *ExperimentAsyncTestKeys, error)
|
||||
}
|
||||
|
||||
// ExperimentCallbacks contains experiment event-handling callbacks
|
||||
type ExperimentCallbacks interface {
|
||||
// OnProgress provides information about an experiment progress.
|
||||
OnProgress(percentage float64, message string)
|
||||
}
|
||||
|
||||
// PrinterCallbacks is the default event handler
|
||||
type PrinterCallbacks struct {
|
||||
Logger
|
||||
}
|
||||
|
||||
// NewPrinterCallbacks returns a new default callback handler
|
||||
func NewPrinterCallbacks(logger Logger) PrinterCallbacks {
|
||||
return PrinterCallbacks{Logger: logger}
|
||||
}
|
||||
|
||||
// OnProgress provides information about an experiment progress.
|
||||
func (d PrinterCallbacks) OnProgress(percentage float64, message string) {
|
||||
d.Logger.Infof("[%5.1f%%] %s", percentage*100, message)
|
||||
}
|
||||
|
||||
// ExperimentMeasurer is the interface that allows to run a
|
||||
// measurement for a specific experiment.
|
||||
type ExperimentMeasurer interface {
|
||||
// ExperimentName returns the experiment name.
|
||||
ExperimentName() string
|
||||
|
||||
// ExperimentVersion returns the experiment version.
|
||||
ExperimentVersion() string
|
||||
|
||||
// Run runs the experiment with the specified context, session,
|
||||
// measurement, and experiment calbacks. This method should only
|
||||
// return an error in case the experiment could not run (e.g.,
|
||||
// a required input is missing). Otherwise, the code should just
|
||||
// set the relevant OONI error inside of the measurement and
|
||||
// return nil. This is important because the caller may not submit
|
||||
// the measurement if this method returns an error.
|
||||
Run(
|
||||
ctx context.Context, sess ExperimentSession,
|
||||
measurement *Measurement, callbacks ExperimentCallbacks,
|
||||
) error
|
||||
|
||||
// GetSummaryKeys returns summary keys expected by ooni/probe-cli.
|
||||
GetSummaryKeys(*Measurement) (interface{}, error)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/apex/log"
|
||||
)
|
||||
|
||||
func TestPrinterCallbacksCallbacks(t *testing.T) {
|
||||
printer := NewPrinterCallbacks(log.Log)
|
||||
printer.OnProgress(0.4, "progress")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
//
|
||||
// Definition of a key-value store.
|
||||
//
|
||||
|
||||
// KeyValueStore is a generic key-value store.
|
||||
type KeyValueStore interface {
|
||||
// Get gets the value of the given key or returns an
|
||||
// error if there is no such key or we cannot read
|
||||
// from the key-value store.
|
||||
Get(key string) (value []byte, err error)
|
||||
|
||||
// Set sets the value of the given key and returns
|
||||
// whether the operation was successful or not.
|
||||
Set(key string, value []byte) (err error)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package model
|
||||
|
||||
//
|
||||
// Logger
|
||||
//
|
||||
|
||||
// DebugLogger is a logger emitting only debug messages.
|
||||
type DebugLogger interface {
|
||||
// Debug emits a debug message.
|
||||
Debug(msg string)
|
||||
|
||||
// Debugf formats and emits a debug message.
|
||||
Debugf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// InfoLogger is a logger emitting debug and infor messages.
|
||||
type InfoLogger interface {
|
||||
// An InfoLogger is also a DebugLogger.
|
||||
DebugLogger
|
||||
|
||||
// Info emits an informational message.
|
||||
Info(msg string)
|
||||
|
||||
// Infof formats and emits an informational message.
|
||||
Infof(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Logger defines the common interface that a logger should have. It is
|
||||
// out of the box compatible with `log.Log` in `apex/log`.
|
||||
type Logger interface {
|
||||
// A Logger is also an InfoLogger.
|
||||
InfoLogger
|
||||
|
||||
// Warn emits a warning message.
|
||||
Warn(msg string)
|
||||
|
||||
// Warnf formats and emits a warning message.
|
||||
Warnf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// DiscardLogger is the default logger that discards its input
|
||||
var DiscardLogger Logger = logDiscarder{}
|
||||
|
||||
// logDiscarder is a logger that discards its input
|
||||
type logDiscarder struct{}
|
||||
|
||||
// Debug implements DebugLogger.Debug
|
||||
func (logDiscarder) Debug(msg string) {}
|
||||
|
||||
// Debugf implements DebugLogger.Debugf
|
||||
func (logDiscarder) Debugf(format string, v ...interface{}) {}
|
||||
|
||||
// Info implements InfoLogger.Info
|
||||
func (logDiscarder) Info(msg string) {}
|
||||
|
||||
// Infov implements InfoLogger.Infov
|
||||
func (logDiscarder) Infof(format string, v ...interface{}) {}
|
||||
|
||||
// Warn implements Logger.Warn
|
||||
func (logDiscarder) Warn(msg string) {}
|
||||
|
||||
// Warnf implements Logger.Warnf
|
||||
func (logDiscarder) Warnf(format string, v ...interface{}) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDiscardLoggerWorksAsIntended(t *testing.T) {
|
||||
logger := DiscardLogger
|
||||
logger.Debug("foo")
|
||||
logger.Debugf("%s", "foo")
|
||||
logger.Info("foo")
|
||||
logger.Infof("%s", "foo")
|
||||
logger.Warn("foo")
|
||||
logger.Warnf("%s", "foo")
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//
|
||||
// Definition of the result of a network measurement.
|
||||
//
|
||||
|
||||
const (
|
||||
// DefaultProbeIP is the default probe IP.
|
||||
DefaultProbeIP = "127.0.0.1"
|
||||
)
|
||||
|
||||
// MeasurementTarget is the target of a OONI measurement.
|
||||
type MeasurementTarget string
|
||||
|
||||
// MarshalJSON serializes the MeasurementTarget.
|
||||
func (t MeasurementTarget) MarshalJSON() ([]byte, error) {
|
||||
if t == "" {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
return json.Marshal(string(t))
|
||||
}
|
||||
|
||||
// Measurement is a OONI measurement.
|
||||
//
|
||||
// This structure is compatible with the definition of the base data format in
|
||||
// https://github.com/ooni/spec/blob/master/data-formats/df-000-base.md.
|
||||
type Measurement struct {
|
||||
// Annotations contains results annotations
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
// DataFormatVersion is the version of the data format
|
||||
DataFormatVersion string `json:"data_format_version"`
|
||||
|
||||
// Extensions contains information about the extensions included
|
||||
// into the test_keys of this measurement.
|
||||
Extensions map[string]int64 `json:"extensions,omitempty"`
|
||||
|
||||
// ID is the locally generated measurement ID
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// Input is the measurement input
|
||||
Input MeasurementTarget `json:"input"`
|
||||
|
||||
// InputHashes contains input hashes
|
||||
InputHashes []string `json:"input_hashes,omitempty"`
|
||||
|
||||
// MeasurementStartTime is the time when the measurement started
|
||||
MeasurementStartTime string `json:"measurement_start_time"`
|
||||
|
||||
// MeasurementStartTimeSaved is the moment in time when we
|
||||
// started the measurement. This is not included into the JSON
|
||||
// and is only used within probe-engine as a "zero" time.
|
||||
MeasurementStartTimeSaved time.Time `json:"-"`
|
||||
|
||||
// Options contains command line options
|
||||
Options []string `json:"options,omitempty"`
|
||||
|
||||
// ProbeASN contains the probe autonomous system number
|
||||
ProbeASN string `json:"probe_asn"`
|
||||
|
||||
// ProbeCC contains the probe country code
|
||||
ProbeCC string `json:"probe_cc"`
|
||||
|
||||
// ProbeCity contains the probe city
|
||||
ProbeCity string `json:"probe_city,omitempty"`
|
||||
|
||||
// ProbeIP contains the probe IP
|
||||
ProbeIP string `json:"probe_ip,omitempty"`
|
||||
|
||||
// ProbeNetworkName contains the probe network name
|
||||
ProbeNetworkName string `json:"probe_network_name"`
|
||||
|
||||
// ReportID contains the report ID
|
||||
ReportID string `json:"report_id"`
|
||||
|
||||
// ResolverASN is the ASN of the resolver
|
||||
ResolverASN string `json:"resolver_asn"`
|
||||
|
||||
// ResolverIP is the resolver IP
|
||||
ResolverIP string `json:"resolver_ip"`
|
||||
|
||||
// ResolverNetworkName is the network name of the resolver.
|
||||
ResolverNetworkName string `json:"resolver_network_name"`
|
||||
|
||||
// SoftwareName contains the software name
|
||||
SoftwareName string `json:"software_name"`
|
||||
|
||||
// SoftwareVersion contains the software version
|
||||
SoftwareVersion string `json:"software_version"`
|
||||
|
||||
// TestHelpers contains the test helpers. It seems this structure is more
|
||||
// complex than we would like. In particular, using a map from string to
|
||||
// string does not fit into the web_connectivity use case. Hence, for now
|
||||
// we're going to represent this using interface{}. In going forward we
|
||||
// may probably want to have more uniform test helpers.
|
||||
TestHelpers map[string]interface{} `json:"test_helpers,omitempty"`
|
||||
|
||||
// TestKeys contains the real test result. This field is opaque because
|
||||
// each experiment will insert here a different structure.
|
||||
TestKeys interface{} `json:"test_keys"`
|
||||
|
||||
// TestName contains the test name
|
||||
TestName string `json:"test_name"`
|
||||
|
||||
// MeasurementRuntime contains the measurement runtime. The JSON name
|
||||
// is test_runtime because this is the name expected by the OONI backend
|
||||
// even though that name is clearly a misleading one.
|
||||
MeasurementRuntime float64 `json:"test_runtime"`
|
||||
|
||||
// TestStartTime contains the test start time
|
||||
TestStartTime string `json:"test_start_time"`
|
||||
|
||||
// TestVersion contains the test version
|
||||
TestVersion string `json:"test_version"`
|
||||
}
|
||||
|
||||
// AddAnnotations adds the annotations from input to m.Annotations.
|
||||
func (m *Measurement) AddAnnotations(input map[string]string) {
|
||||
for key, value := range input {
|
||||
m.AddAnnotation(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// AddAnnotation adds a single annotations to m.Annotations.
|
||||
func (m *Measurement) AddAnnotation(key, value string) {
|
||||
if m.Annotations == nil {
|
||||
m.Annotations = make(map[string]string)
|
||||
}
|
||||
m.Annotations[key] = value
|
||||
}
|
||||
|
||||
// ErrInvalidProbeIP indicates that we're dealing with a string that
|
||||
// is not the valid serialization of an IP address.
|
||||
var ErrInvalidProbeIP = errors.New("model: invalid probe IP")
|
||||
|
||||
// Scrub scrubs the probeIP out of the measurement.
|
||||
func (m *Measurement) Scrub(probeIP string) (err error) {
|
||||
// We now behave like we can share everything except the
|
||||
// probe IP, which we instead cannot ever share
|
||||
m.ProbeIP = DefaultProbeIP
|
||||
return m.MaybeRewriteTestKeys(probeIP, json.Marshal)
|
||||
}
|
||||
|
||||
// Scrubbed is the string that replaces IP addresses.
|
||||
const Scrubbed = `[scrubbed]`
|
||||
|
||||
// MaybeRewriteTestKeys is the function called by Scrub that
|
||||
// ensures that m's serialization doesn't include the IP
|
||||
func (m *Measurement) MaybeRewriteTestKeys(
|
||||
currentIP string, marshal func(interface{}) ([]byte, error)) error {
|
||||
if net.ParseIP(currentIP) == nil {
|
||||
return ErrInvalidProbeIP
|
||||
}
|
||||
data, err := marshal(m.TestKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The check using Count is to save an unnecessary copy performed by
|
||||
// ReplaceAll when there are no matches into the body. This is what
|
||||
// we would like the common case to be, meaning that the code has done
|
||||
// its job correctly and has not leaked the IP.
|
||||
bpip := []byte(currentIP)
|
||||
if bytes.Count(data, bpip) <= 0 {
|
||||
return nil
|
||||
}
|
||||
data = bytes.ReplaceAll(data, bpip, []byte(Scrubbed))
|
||||
// We add an annotation such that hopefully later we can measure the
|
||||
// number of cases where we failed to sanitize properly.
|
||||
m.AddAnnotation("_probe_engine_sanitize_test_keys", "true")
|
||||
return json.Unmarshal(data, &m.TestKeys)
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMeasurementTargetMarshalJSON(t *testing.T) {
|
||||
var mt MeasurementTarget
|
||||
data, err := json.Marshal(mt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "null" {
|
||||
t.Fatal("unexpected serialization")
|
||||
}
|
||||
mt = "xx"
|
||||
data, err = json.Marshal(mt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != `"xx"` {
|
||||
t.Fatal("unexpected serialization")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeTestKeys struct {
|
||||
ClientResolver string `json:"client_resolver"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
func TestAddAnnotations(t *testing.T) {
|
||||
m := &Measurement{}
|
||||
m.AddAnnotations(map[string]string{
|
||||
"foo": "bar",
|
||||
"f": "b",
|
||||
})
|
||||
m.AddAnnotations(map[string]string{
|
||||
"foobar": "bar",
|
||||
"f": "b",
|
||||
})
|
||||
if len(m.Annotations) != 3 {
|
||||
t.Fatal("unexpected number of annotations")
|
||||
}
|
||||
if m.Annotations["foo"] != "bar" {
|
||||
t.Fatal("unexpected annotation")
|
||||
}
|
||||
if m.Annotations["f"] != "b" {
|
||||
t.Fatal("unexpected annotation")
|
||||
}
|
||||
if m.Annotations["foobar"] != "bar" {
|
||||
t.Fatal("unexpected annotation")
|
||||
}
|
||||
}
|
||||
|
||||
type makeMeasurementConfig struct {
|
||||
ProbeIP string
|
||||
ProbeASN string
|
||||
ProbeNetworkName string
|
||||
ProbeCC string
|
||||
ResolverIP string
|
||||
ResolverNetworkName string
|
||||
ResolverASN string
|
||||
}
|
||||
|
||||
func makeMeasurement(config makeMeasurementConfig) Measurement {
|
||||
return Measurement{
|
||||
DataFormatVersion: "0.3.0",
|
||||
ID: "bdd20d7a-bba5-40dd-a111-9863d7908572",
|
||||
MeasurementStartTime: "2018-11-01 15:33:20",
|
||||
ProbeIP: config.ProbeIP,
|
||||
ProbeASN: config.ProbeASN,
|
||||
ProbeNetworkName: config.ProbeNetworkName,
|
||||
ProbeCC: config.ProbeCC,
|
||||
ReportID: "",
|
||||
ResolverIP: config.ResolverIP,
|
||||
ResolverNetworkName: config.ResolverNetworkName,
|
||||
ResolverASN: config.ResolverASN,
|
||||
SoftwareName: "probe-engine",
|
||||
SoftwareVersion: "0.1.0",
|
||||
TestKeys: &fakeTestKeys{
|
||||
ClientResolver: "91.80.37.104",
|
||||
Body: fmt.Sprintf(`
|
||||
<HTML><HEAD><TITLE>Your IP is %s</TITLE></HEAD>
|
||||
<BODY><P>Hey you, I see your IP and it's %s!</P></BODY>
|
||||
`, config.ProbeIP, config.ProbeIP),
|
||||
},
|
||||
TestName: "dummy",
|
||||
MeasurementRuntime: 5.0565230846405,
|
||||
TestStartTime: "2018-11-01 15:33:17",
|
||||
TestVersion: "0.1.0",
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubWeAreScrubbing(t *testing.T) {
|
||||
config := makeMeasurementConfig{
|
||||
ProbeIP: "130.192.91.211",
|
||||
ProbeASN: "AS137",
|
||||
ProbeCC: "IT",
|
||||
ProbeNetworkName: "Vodafone Italia S.p.A.",
|
||||
ResolverIP: "8.8.8.8",
|
||||
ResolverNetworkName: "Google LLC",
|
||||
ResolverASN: "AS12345",
|
||||
}
|
||||
m := makeMeasurement(config)
|
||||
if err := m.Scrub(config.ProbeIP); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.ProbeASN != config.ProbeASN {
|
||||
t.Fatal("ProbeASN has been scrubbed")
|
||||
}
|
||||
if m.ProbeCC != config.ProbeCC {
|
||||
t.Fatal("ProbeCC has been scrubbed")
|
||||
}
|
||||
if m.ProbeIP == config.ProbeIP {
|
||||
t.Fatal("ProbeIP HAS NOT been scrubbed")
|
||||
}
|
||||
if m.ProbeNetworkName != config.ProbeNetworkName {
|
||||
t.Fatal("ProbeNetworkName has been scrubbed")
|
||||
}
|
||||
if m.ResolverIP != config.ResolverIP {
|
||||
t.Fatal("ResolverIP has been scrubbed")
|
||||
}
|
||||
if m.ResolverNetworkName != config.ResolverNetworkName {
|
||||
t.Fatal("ResolverNetworkName has been scrubbed")
|
||||
}
|
||||
if m.ResolverASN != config.ResolverASN {
|
||||
t.Fatal("ResolverASN has been scrubbed")
|
||||
}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Count(data, []byte(config.ProbeIP)) != 0 {
|
||||
t.Fatal("ProbeIP not fully redacted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubNoScrubbingRequired(t *testing.T) {
|
||||
config := makeMeasurementConfig{
|
||||
ProbeIP: "130.192.91.211",
|
||||
ProbeASN: "AS137",
|
||||
ProbeCC: "IT",
|
||||
ProbeNetworkName: "Vodafone Italia S.p.A.",
|
||||
ResolverIP: "8.8.8.8",
|
||||
ResolverNetworkName: "Google LLC",
|
||||
ResolverASN: "AS12345",
|
||||
}
|
||||
m := makeMeasurement(config)
|
||||
m.TestKeys.(*fakeTestKeys).Body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
if err := m.Scrub(config.ProbeIP); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.ProbeASN != config.ProbeASN {
|
||||
t.Fatal("ProbeASN has been scrubbed")
|
||||
}
|
||||
if m.ProbeCC != config.ProbeCC {
|
||||
t.Fatal("ProbeCC has been scrubbed")
|
||||
}
|
||||
if m.ProbeIP == config.ProbeIP {
|
||||
t.Fatal("ProbeIP HAS NOT been scrubbed")
|
||||
}
|
||||
if m.ProbeNetworkName != config.ProbeNetworkName {
|
||||
t.Fatal("ProbeNetworkName has been scrubbed")
|
||||
}
|
||||
if m.ResolverIP != config.ResolverIP {
|
||||
t.Fatal("ResolverIP has been scrubbed")
|
||||
}
|
||||
if m.ResolverNetworkName != config.ResolverNetworkName {
|
||||
t.Fatal("ResolverNetworkName has been scrubbed")
|
||||
}
|
||||
if m.ResolverASN != config.ResolverASN {
|
||||
t.Fatal("ResolverASN has been scrubbed")
|
||||
}
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Count(data, []byte(Scrubbed)) > 0 {
|
||||
t.Fatal("We should not see any scrubbing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubInvalidIP(t *testing.T) {
|
||||
m := &Measurement{
|
||||
ProbeASN: "AS1234",
|
||||
ProbeCC: "IT",
|
||||
}
|
||||
err := m.Scrub("") // invalid IP
|
||||
if !errors.Is(err, ErrInvalidProbeIP) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScrubMarshalError(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
m := &Measurement{
|
||||
ProbeASN: "AS1234",
|
||||
ProbeCC: "IT",
|
||||
}
|
||||
err := m.MaybeRewriteTestKeys(
|
||||
"8.8.8.8", func(v interface{}) ([]byte, error) {
|
||||
return nil, expected
|
||||
})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Dialer is a mockable Dialer.
|
||||
type Dialer struct {
|
||||
MockDialContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
MockCloseIdleConnections func()
|
||||
}
|
||||
|
||||
// DialContext calls MockDialContext.
|
||||
func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return d.MockDialContext(ctx, network, address)
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (d *Dialer) CloseIdleConnections() {
|
||||
d.MockCloseIdleConnections()
|
||||
}
|
||||
|
||||
// Conn is a mockable net.Conn.
|
||||
type Conn struct {
|
||||
MockRead func(b []byte) (int, error)
|
||||
MockWrite func(b []byte) (int, error)
|
||||
MockClose func() error
|
||||
MockLocalAddr func() net.Addr
|
||||
MockRemoteAddr func() net.Addr
|
||||
MockSetDeadline func(t time.Time) error
|
||||
MockSetReadDeadline func(t time.Time) error
|
||||
MockSetWriteDeadline func(t time.Time) error
|
||||
}
|
||||
|
||||
// Read calls MockRead.
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
return c.MockRead(b)
|
||||
}
|
||||
|
||||
// Write calls MockWrite.
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
return c.MockWrite(b)
|
||||
}
|
||||
|
||||
// Close calls MockClose.
|
||||
func (c *Conn) Close() error {
|
||||
return c.MockClose()
|
||||
}
|
||||
|
||||
// LocalAddr calls MockLocalAddr.
|
||||
func (c *Conn) LocalAddr() net.Addr {
|
||||
return c.MockLocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr calls MockRemoteAddr.
|
||||
func (c *Conn) RemoteAddr() net.Addr {
|
||||
return c.MockRemoteAddr()
|
||||
}
|
||||
|
||||
// SetDeadline calls MockSetDeadline.
|
||||
func (c *Conn) SetDeadline(t time.Time) error {
|
||||
return c.MockSetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline calls MockSetReadDeadline.
|
||||
func (c *Conn) SetReadDeadline(t time.Time) error {
|
||||
return c.MockSetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline calls MockSetWriteDeadline.
|
||||
func (c *Conn) SetWriteDeadline(t time.Time) error {
|
||||
return c.MockSetWriteDeadline(t)
|
||||
}
|
||||
|
||||
var _ net.Conn = &Conn{}
|
||||
@@ -0,0 +1,161 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestDialer(t *testing.T) {
|
||||
t.Run("DialContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
d := Dialer{
|
||||
MockDialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn, err := d.DialContext(ctx, "tcp", "8.8.8.8:53")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil conn")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
var called bool
|
||||
d := &Dialer{
|
||||
MockCloseIdleConnections: func() {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
d.CloseIdleConnections()
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConn(t *testing.T) {
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockRead: func(b []byte) (int, error) {
|
||||
return 0, expected
|
||||
},
|
||||
}
|
||||
count, err := c.Read(make([]byte, 128))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("expected 0 bytes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Write", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockWrite: func(b []byte) (int, error) {
|
||||
return 0, expected
|
||||
},
|
||||
}
|
||||
count, err := c.Write(make([]byte, 128))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("expected 0 bytes")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Close", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockClose: func() error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.Close()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LocalAddr", func(t *testing.T) {
|
||||
expected := &net.TCPAddr{
|
||||
IP: net.IPv6loopback,
|
||||
Port: 1234,
|
||||
}
|
||||
c := &Conn{
|
||||
MockLocalAddr: func() net.Addr {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
out := c.LocalAddr()
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RemoteAddr", func(t *testing.T) {
|
||||
expected := &net.TCPAddr{
|
||||
IP: net.IPv6loopback,
|
||||
Port: 1234,
|
||||
}
|
||||
c := &Conn{
|
||||
MockRemoteAddr: func() net.Addr {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
out := c.RemoteAddr()
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockSetDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetReadDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockSetReadDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetReadDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetWriteDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &Conn{
|
||||
MockSetWriteDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetWriteDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package mocks
|
||||
|
||||
import "github.com/ooni/probe-cli/v3/internal/model"
|
||||
|
||||
// DNSDecoder allows mocking dnsx.DNSDecoder.
|
||||
type DNSDecoder struct {
|
||||
MockDecodeLookupHost func(qtype uint16, reply []byte) ([]string, error)
|
||||
|
||||
MockDecodeHTTPS func(reply []byte) (*model.HTTPSSvc, error)
|
||||
}
|
||||
|
||||
// DecodeLookupHost calls MockDecodeLookupHost.
|
||||
func (e *DNSDecoder) DecodeLookupHost(qtype uint16, reply []byte) ([]string, error) {
|
||||
return e.MockDecodeLookupHost(qtype, reply)
|
||||
}
|
||||
|
||||
// DecodeHTTPS calls MockDecodeHTTPS.
|
||||
func (e *DNSDecoder) DecodeHTTPS(reply []byte) (*model.HTTPSSvc, error) {
|
||||
return e.MockDecodeHTTPS(reply)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestDNSDecoder(t *testing.T) {
|
||||
t.Run("DecodeLookupHost", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
e := &DNSDecoder{
|
||||
MockDecodeLookupHost: func(qtype uint16, reply []byte) ([]string, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.DecodeLookupHost(dns.TypeA, make([]byte, 17))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("unexpected out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DecodeHTTPS", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
e := &DNSDecoder{
|
||||
MockDecodeHTTPS: func(reply []byte) (*model.HTTPSSvc, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.DecodeHTTPS(make([]byte, 17))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("unexpected out")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package mocks
|
||||
|
||||
// DNSEncoder allows mocking dnsx.DNSEncoder.
|
||||
type DNSEncoder struct {
|
||||
MockEncode func(domain string, qtype uint16, padding bool) ([]byte, error)
|
||||
}
|
||||
|
||||
// Encode calls MockEncode.
|
||||
func (e *DNSEncoder) Encode(domain string, qtype uint16, padding bool) ([]byte, error) {
|
||||
return e.MockEncode(domain, qtype, padding)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func TestDNSEncoder(t *testing.T) {
|
||||
t.Run("Encode", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
e := &DNSEncoder{
|
||||
MockEncode: func(domain string, qtype uint16, padding bool) ([]byte, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
out, err := e.Encode("dns.google", dns.TypeA, true)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if out != nil {
|
||||
t.Fatal("unexpected out")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package mocks
|
||||
|
||||
import "context"
|
||||
|
||||
// DNSTransport allows mocking dnsx.DNSTransport.
|
||||
type DNSTransport struct {
|
||||
MockRoundTrip func(ctx context.Context, query []byte) (reply []byte, err error)
|
||||
|
||||
MockRequiresPadding func() bool
|
||||
|
||||
MockNetwork func() string
|
||||
|
||||
MockAddress func() string
|
||||
|
||||
MockCloseIdleConnections func()
|
||||
}
|
||||
|
||||
// RoundTrip calls MockRoundTrip.
|
||||
func (txp *DNSTransport) RoundTrip(ctx context.Context, query []byte) (reply []byte, err error) {
|
||||
return txp.MockRoundTrip(ctx, query)
|
||||
}
|
||||
|
||||
// RequiresPadding calls MockRequiresPadding.
|
||||
func (txp *DNSTransport) RequiresPadding() bool {
|
||||
return txp.MockRequiresPadding()
|
||||
}
|
||||
|
||||
// Network calls MockNetwork.
|
||||
func (txp *DNSTransport) Network() string {
|
||||
return txp.MockNetwork()
|
||||
}
|
||||
|
||||
// Address calls MockAddress.
|
||||
func (txp *DNSTransport) Address() string {
|
||||
return txp.MockAddress()
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (txp *DNSTransport) CloseIdleConnections() {
|
||||
txp.MockCloseIdleConnections()
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
)
|
||||
|
||||
func TestDNSTransport(t *testing.T) {
|
||||
t.Run("RoundTrip", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
txp := &DNSTransport{
|
||||
MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
resp, err := txp.RoundTrip(context.Background(), make([]byte, 16))
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected nil response here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RequiresPadding", func(t *testing.T) {
|
||||
txp := &DNSTransport{
|
||||
MockRequiresPadding: func() bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
if txp.RequiresPadding() != true {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Network", func(t *testing.T) {
|
||||
txp := &DNSTransport{
|
||||
MockNetwork: func() string {
|
||||
return "antani"
|
||||
},
|
||||
}
|
||||
if txp.Network() != "antani" {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Address", func(t *testing.T) {
|
||||
txp := &DNSTransport{
|
||||
MockAddress: func() string {
|
||||
return "mascetti"
|
||||
},
|
||||
}
|
||||
if txp.Address() != "mascetti" {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
called := &atomicx.Int64{}
|
||||
txp := &DNSTransport{
|
||||
MockCloseIdleConnections: func() {
|
||||
called.Add(1)
|
||||
},
|
||||
}
|
||||
txp.CloseIdleConnections()
|
||||
if called.Load() != 1 {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package mocks contains mocks for internal/model interfaces.
|
||||
package mocks
|
||||
@@ -0,0 +1,36 @@
|
||||
package mocks
|
||||
|
||||
import "net/http"
|
||||
|
||||
// HTTPTransport mocks netxlite.HTTPTransport.
|
||||
type HTTPTransport struct {
|
||||
MockRoundTrip func(req *http.Request) (*http.Response, error)
|
||||
MockCloseIdleConnections func()
|
||||
}
|
||||
|
||||
// RoundTrip calls MockRoundTrip.
|
||||
func (txp *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
return txp.MockRoundTrip(req)
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (txp *HTTPTransport) CloseIdleConnections() {
|
||||
txp.MockCloseIdleConnections()
|
||||
}
|
||||
|
||||
// HTTPClient allows mocking an http.Client.
|
||||
type HTTPClient struct {
|
||||
MockDo func(req *http.Request) (*http.Response, error)
|
||||
|
||||
MockCloseIdleConnections func()
|
||||
}
|
||||
|
||||
// Do calls MockDo.
|
||||
func (txp *HTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
return txp.MockDo(req)
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (txp *HTTPClient) CloseIdleConnections() {
|
||||
txp.MockCloseIdleConnections()
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/atomicx"
|
||||
)
|
||||
|
||||
func TestHTTPTransport(t *testing.T) {
|
||||
t.Run("RoundTrip", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
txp := &HTTPTransport{
|
||||
MockRoundTrip: func(req *http.Request) (*http.Response, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
resp, err := txp.RoundTrip(&http.Request{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected nil response here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
called := &atomicx.Int64{}
|
||||
txp := &HTTPTransport{
|
||||
MockCloseIdleConnections: func() {
|
||||
called.Add(1)
|
||||
},
|
||||
}
|
||||
txp.CloseIdleConnections()
|
||||
if called.Load() != 1 {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPClient(t *testing.T) {
|
||||
t.Run("Do", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
clnt := &HTTPClient{
|
||||
MockDo: func(req *http.Request) (*http.Response, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
resp, err := clnt.Do(&http.Request{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if resp != nil {
|
||||
t.Fatal("expected nil response here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
called := &atomicx.Int64{}
|
||||
clnt := &HTTPClient{
|
||||
MockCloseIdleConnections: func() {
|
||||
called.Add(1)
|
||||
},
|
||||
}
|
||||
clnt.CloseIdleConnections()
|
||||
if called.Load() != 1 {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package mocks
|
||||
|
||||
import "net"
|
||||
|
||||
// Listener allows mocking a net.Listener.
|
||||
type Listener struct {
|
||||
// Accept allows mocking Accept.
|
||||
MockAccept func() (net.Conn, error)
|
||||
|
||||
// Close allows mocking Close.
|
||||
MockClose func() error
|
||||
|
||||
// Addr allows mocking Addr.
|
||||
MockAddr func() net.Addr
|
||||
}
|
||||
|
||||
var _ net.Listener = &Listener{}
|
||||
|
||||
// Accept implements net.Listener.Accept
|
||||
func (li *Listener) Accept() (net.Conn, error) {
|
||||
return li.MockAccept()
|
||||
}
|
||||
|
||||
// Close implements net.Listener.Closer.
|
||||
func (li *Listener) Close() error {
|
||||
return li.MockClose()
|
||||
}
|
||||
|
||||
// Addr implements net.Listener.Addr
|
||||
func (li *Listener) Addr() net.Addr {
|
||||
return li.MockAddr()
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestListener(t *testing.T) {
|
||||
t.Run("Accept", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
li := &Listener{
|
||||
MockAccept: func() (net.Conn, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
conn, err := li.Accept()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil conn")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Close", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
li := &Listener{
|
||||
MockClose: func() error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := li.Close()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected err", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Addr", func(t *testing.T) {
|
||||
addr := &net.TCPAddr{
|
||||
IP: net.IPv4(127, 0, 0, 1),
|
||||
Port: 1234,
|
||||
}
|
||||
li := &Listener{
|
||||
MockAddr: func() net.Addr {
|
||||
return addr
|
||||
},
|
||||
}
|
||||
outAddr := li.Addr()
|
||||
if diff := cmp.Diff(addr, outAddr); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package mocks
|
||||
|
||||
// Logger allows mocking a logger.
|
||||
type Logger struct {
|
||||
MockDebug func(message string)
|
||||
|
||||
MockDebugf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// Debug calls MockDebug.
|
||||
func (lo *Logger) Debug(message string) {
|
||||
lo.MockDebug(message)
|
||||
}
|
||||
|
||||
// Debugf calls MockDebugf.
|
||||
func (lo *Logger) Debugf(format string, v ...interface{}) {
|
||||
lo.MockDebugf(format, v...)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package mocks
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLogger(t *testing.T) {
|
||||
t.Run("Debug", func(t *testing.T) {
|
||||
var called bool
|
||||
lo := &Logger{
|
||||
MockDebug: func(message string) {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
lo.Debug("antani")
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Debugf", func(t *testing.T) {
|
||||
var called bool
|
||||
lo := &Logger{
|
||||
MockDebugf: func(message string, v ...interface{}) {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
lo.Debugf("antani", 1, 2, 3, 4)
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// QUICListener is a mockable netxlite.QUICListener.
|
||||
type QUICListener struct {
|
||||
MockListen func(addr *net.UDPAddr) (model.UDPLikeConn, error)
|
||||
}
|
||||
|
||||
// Listen calls MockListen.
|
||||
func (ql *QUICListener) Listen(addr *net.UDPAddr) (model.UDPLikeConn, error) {
|
||||
return ql.MockListen(addr)
|
||||
}
|
||||
|
||||
// QUICDialer is a mockable netxlite.QUICDialer.
|
||||
type QUICDialer struct {
|
||||
// MockDialContext allows mocking DialContext.
|
||||
MockDialContext func(ctx context.Context, network, address string,
|
||||
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
|
||||
|
||||
// MockCloseIdleConnections allows mocking CloseIdleConnections.
|
||||
MockCloseIdleConnections func()
|
||||
}
|
||||
|
||||
// DialContext calls MockDialContext.
|
||||
func (qcd *QUICDialer) DialContext(ctx context.Context, network, address string,
|
||||
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
|
||||
return qcd.MockDialContext(ctx, network, address, tlsConfig, quicConfig)
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (qcd *QUICDialer) CloseIdleConnections() {
|
||||
qcd.MockCloseIdleConnections()
|
||||
}
|
||||
|
||||
// QUICEarlySession is a mockable quic.EarlySession.
|
||||
type QUICEarlySession struct {
|
||||
MockAcceptStream func(context.Context) (quic.Stream, error)
|
||||
MockAcceptUniStream func(context.Context) (quic.ReceiveStream, error)
|
||||
MockOpenStream func() (quic.Stream, error)
|
||||
MockOpenStreamSync func(ctx context.Context) (quic.Stream, error)
|
||||
MockOpenUniStream func() (quic.SendStream, error)
|
||||
MockOpenUniStreamSync func(ctx context.Context) (quic.SendStream, error)
|
||||
MockLocalAddr func() net.Addr
|
||||
MockRemoteAddr func() net.Addr
|
||||
MockCloseWithError func(code quic.ApplicationErrorCode, reason string) error
|
||||
MockContext func() context.Context
|
||||
MockConnectionState func() quic.ConnectionState
|
||||
MockHandshakeComplete func() context.Context
|
||||
MockNextSession func() quic.Session
|
||||
MockSendMessage func(b []byte) error
|
||||
MockReceiveMessage func() ([]byte, error)
|
||||
}
|
||||
|
||||
var _ quic.EarlySession = &QUICEarlySession{}
|
||||
|
||||
// AcceptStream calls MockAcceptStream.
|
||||
func (s *QUICEarlySession) AcceptStream(ctx context.Context) (quic.Stream, error) {
|
||||
return s.MockAcceptStream(ctx)
|
||||
}
|
||||
|
||||
// AcceptUniStream calls MockAcceptUniStream.
|
||||
func (s *QUICEarlySession) AcceptUniStream(ctx context.Context) (quic.ReceiveStream, error) {
|
||||
return s.MockAcceptUniStream(ctx)
|
||||
}
|
||||
|
||||
// OpenStream calls MockOpenStream.
|
||||
func (s *QUICEarlySession) OpenStream() (quic.Stream, error) {
|
||||
return s.MockOpenStream()
|
||||
}
|
||||
|
||||
// OpenStreamSync calls MockOpenStreamSync.
|
||||
func (s *QUICEarlySession) OpenStreamSync(ctx context.Context) (quic.Stream, error) {
|
||||
return s.MockOpenStreamSync(ctx)
|
||||
}
|
||||
|
||||
// OpenUniStream calls MockOpenUniStream.
|
||||
func (s *QUICEarlySession) OpenUniStream() (quic.SendStream, error) {
|
||||
return s.MockOpenUniStream()
|
||||
}
|
||||
|
||||
// OpenUniStreamSync calls MockOpenUniStreamSync.
|
||||
func (s *QUICEarlySession) OpenUniStreamSync(ctx context.Context) (quic.SendStream, error) {
|
||||
return s.MockOpenUniStreamSync(ctx)
|
||||
}
|
||||
|
||||
// LocalAddr class MockLocalAddr.
|
||||
func (c *QUICEarlySession) LocalAddr() net.Addr {
|
||||
return c.MockLocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr calls MockRemoteAddr.
|
||||
func (c *QUICEarlySession) RemoteAddr() net.Addr {
|
||||
return c.MockRemoteAddr()
|
||||
}
|
||||
|
||||
// CloseWithError calls MockCloseWithError.
|
||||
func (c *QUICEarlySession) CloseWithError(
|
||||
code quic.ApplicationErrorCode, reason string) error {
|
||||
return c.MockCloseWithError(code, reason)
|
||||
}
|
||||
|
||||
// Context calls MockContext.
|
||||
func (s *QUICEarlySession) Context() context.Context {
|
||||
return s.MockContext()
|
||||
}
|
||||
|
||||
// ConnectionState calls MockConnectionState.
|
||||
func (s *QUICEarlySession) ConnectionState() quic.ConnectionState {
|
||||
return s.MockConnectionState()
|
||||
}
|
||||
|
||||
// HandshakeComplete calls MockHandshakeComplete.
|
||||
func (s *QUICEarlySession) HandshakeComplete() context.Context {
|
||||
return s.MockHandshakeComplete()
|
||||
}
|
||||
|
||||
// NextSession calls MockNextSession.
|
||||
func (s *QUICEarlySession) NextSession() quic.Session {
|
||||
return s.MockNextSession()
|
||||
}
|
||||
|
||||
// SendMessage calls MockSendMessage.
|
||||
func (s *QUICEarlySession) SendMessage(b []byte) error {
|
||||
return s.MockSendMessage(b)
|
||||
}
|
||||
|
||||
// ReceiveMessage calls MockReceiveMessage.
|
||||
func (s *QUICEarlySession) ReceiveMessage() ([]byte, error) {
|
||||
return s.MockReceiveMessage()
|
||||
}
|
||||
|
||||
// QUICUDPLikeConn is an UDP conn used by QUIC.
|
||||
type QUICUDPLikeConn struct {
|
||||
MockWriteTo func(p []byte, addr net.Addr) (int, error)
|
||||
MockClose func() error
|
||||
MockLocalAddr func() net.Addr
|
||||
MockRemoteAddr func() net.Addr
|
||||
MockSetDeadline func(t time.Time) error
|
||||
MockSetReadDeadline func(t time.Time) error
|
||||
MockSetWriteDeadline func(t time.Time) error
|
||||
MockReadFrom func(p []byte) (n int, addr net.Addr, err error)
|
||||
MockSyscallConn func() (syscall.RawConn, error)
|
||||
MockSetReadBuffer func(n int) error
|
||||
}
|
||||
|
||||
var _ model.UDPLikeConn = &QUICUDPLikeConn{}
|
||||
|
||||
// WriteTo calls MockWriteTo.
|
||||
func (c *QUICUDPLikeConn) WriteTo(p []byte, addr net.Addr) (int, error) {
|
||||
return c.MockWriteTo(p, addr)
|
||||
}
|
||||
|
||||
// Close calls MockClose.
|
||||
func (c *QUICUDPLikeConn) Close() error {
|
||||
return c.MockClose()
|
||||
}
|
||||
|
||||
// LocalAddr calls MockLocalAddr.
|
||||
func (c *QUICUDPLikeConn) LocalAddr() net.Addr {
|
||||
return c.MockLocalAddr()
|
||||
}
|
||||
|
||||
// RemoteAddr calls MockRemoteAddr.
|
||||
func (c *QUICUDPLikeConn) RemoteAddr() net.Addr {
|
||||
return c.MockRemoteAddr()
|
||||
}
|
||||
|
||||
// SetDeadline calls MockSetDeadline.
|
||||
func (c *QUICUDPLikeConn) SetDeadline(t time.Time) error {
|
||||
return c.MockSetDeadline(t)
|
||||
}
|
||||
|
||||
// SetReadDeadline calls MockSetReadDeadline.
|
||||
func (c *QUICUDPLikeConn) SetReadDeadline(t time.Time) error {
|
||||
return c.MockSetReadDeadline(t)
|
||||
}
|
||||
|
||||
// SetWriteDeadline calls MockSetWriteDeadline.
|
||||
func (c *QUICUDPLikeConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.MockSetWriteDeadline(t)
|
||||
}
|
||||
|
||||
// ReadFrom calls MockReadFrom.
|
||||
func (c *QUICUDPLikeConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
return c.MockReadFrom(b)
|
||||
}
|
||||
|
||||
// SyscallConn calls MockSyscallConn.
|
||||
func (c *QUICUDPLikeConn) SyscallConn() (syscall.RawConn, error) {
|
||||
return c.MockSyscallConn()
|
||||
}
|
||||
|
||||
// SetReadBuffer calls MockSetReadBuffer.
|
||||
func (c *QUICUDPLikeConn) SetReadBuffer(n int) error {
|
||||
return c.MockSetReadBuffer(n)
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestQUICListenerListen(t *testing.T) {
|
||||
t.Run("Listen", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
ql := &QUICListener{
|
||||
MockListen: func(addr *net.UDPAddr) (model.UDPLikeConn, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
pconn, err := ql.Listen(&net.UDPAddr{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", expected)
|
||||
}
|
||||
if pconn != nil {
|
||||
t.Fatal("expected nil conn here")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQUICDialer(t *testing.T) {
|
||||
t.Run("DialContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
qcd := &QUICDialer{
|
||||
MockDialContext: func(ctx context.Context, network string, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
tlsConfig := &tls.Config{}
|
||||
quicConfig := &quic.Config{}
|
||||
sess, err := qcd.DialContext(ctx, "udp", "dns.google:443", tlsConfig, quicConfig)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected")
|
||||
}
|
||||
if sess != nil {
|
||||
t.Fatal("expected nil session")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
var called bool
|
||||
qcd := &QUICDialer{
|
||||
MockCloseIdleConnections: func() {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
qcd.CloseIdleConnections()
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQUICEarlySession(t *testing.T) {
|
||||
t.Run("AcceptStream", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockAcceptStream: func(ctx context.Context) (quic.Stream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
stream, err := sess.AcceptStream(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("AcceptUniStream", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockAcceptUniStream: func(ctx context.Context) (quic.ReceiveStream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
stream, err := sess.AcceptUniStream(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenStream", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockOpenStream: func() (quic.Stream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
stream, err := sess.OpenStream()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenStreamSync", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockOpenStreamSync: func(ctx context.Context) (quic.Stream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
stream, err := sess.OpenStreamSync(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenUniStream", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockOpenUniStream: func() (quic.SendStream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
stream, err := sess.OpenUniStream()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenUniStreamSync", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockOpenUniStreamSync: func(ctx context.Context) (quic.SendStream, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
stream, err := sess.OpenUniStreamSync(ctx)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if stream != nil {
|
||||
t.Fatal("expected nil stream here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LocalAddr", func(t *testing.T) {
|
||||
sess := &QUICEarlySession{
|
||||
MockLocalAddr: func() net.Addr {
|
||||
return &net.UDPAddr{}
|
||||
},
|
||||
}
|
||||
addr := sess.LocalAddr()
|
||||
if !reflect.ValueOf(addr).Elem().IsZero() {
|
||||
t.Fatal("expected a zero address here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RemoteAddr", func(t *testing.T) {
|
||||
sess := &QUICEarlySession{
|
||||
MockRemoteAddr: func() net.Addr {
|
||||
return &net.UDPAddr{}
|
||||
},
|
||||
}
|
||||
addr := sess.RemoteAddr()
|
||||
if !reflect.ValueOf(addr).Elem().IsZero() {
|
||||
t.Fatal("expected a zero address here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseWithError", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockCloseWithError: func(
|
||||
code quic.ApplicationErrorCode, reason string) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := sess.CloseWithError(0, "")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Context", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sess := &QUICEarlySession{
|
||||
MockContext: func() context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
out := sess.Context()
|
||||
if !reflect.DeepEqual(ctx, out) {
|
||||
t.Fatal("not the context we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConnectionState", func(t *testing.T) {
|
||||
state := quic.ConnectionState{SupportsDatagrams: true}
|
||||
sess := &QUICEarlySession{
|
||||
MockConnectionState: func() quic.ConnectionState {
|
||||
return state
|
||||
},
|
||||
}
|
||||
out := sess.ConnectionState()
|
||||
if !reflect.DeepEqual(state, out) {
|
||||
t.Fatal("not the context we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HandshakeComplete", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
sess := &QUICEarlySession{
|
||||
MockHandshakeComplete: func() context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
out := sess.HandshakeComplete()
|
||||
if !reflect.DeepEqual(ctx, out) {
|
||||
t.Fatal("not the context we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NextSession", func(t *testing.T) {
|
||||
next := &QUICEarlySession{}
|
||||
sess := &QUICEarlySession{
|
||||
MockNextSession: func() quic.Session {
|
||||
return next
|
||||
},
|
||||
}
|
||||
out := sess.NextSession()
|
||||
if !reflect.DeepEqual(next, out) {
|
||||
t.Fatal("not the context we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SendMessage", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockSendMessage: func(b []byte) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
b := make([]byte, 17)
|
||||
err := sess.SendMessage(b)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ReceiveMessage", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
sess := &QUICEarlySession{
|
||||
MockReceiveMessage: func() ([]byte, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
b, err := sess.ReceiveMessage()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if b != nil {
|
||||
t.Fatal("expected nil buffer here")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQUICUDPLikeConn(t *testing.T) {
|
||||
t.Run("WriteTo", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
quc := &QUICUDPLikeConn{
|
||||
MockWriteTo: func(p []byte, addr net.Addr) (int, error) {
|
||||
return 0, expected
|
||||
},
|
||||
}
|
||||
pkt := make([]byte, 128)
|
||||
addr := &net.UDPAddr{}
|
||||
cnt, err := quc.WriteTo(pkt, addr)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if cnt != 0 {
|
||||
t.Fatal("expected zero here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConnClose", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
quc := &QUICUDPLikeConn{
|
||||
MockClose: func() error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := quc.Close()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LocalAddr", func(t *testing.T) {
|
||||
expected := &net.TCPAddr{
|
||||
IP: net.IPv6loopback,
|
||||
Port: 1234,
|
||||
}
|
||||
c := &QUICUDPLikeConn{
|
||||
MockLocalAddr: func() net.Addr {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
out := c.LocalAddr()
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RemoteAddr", func(t *testing.T) {
|
||||
expected := &net.TCPAddr{
|
||||
IP: net.IPv6loopback,
|
||||
Port: 1234,
|
||||
}
|
||||
c := &QUICUDPLikeConn{
|
||||
MockRemoteAddr: func() net.Addr {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
out := c.RemoteAddr()
|
||||
if diff := cmp.Diff(expected, out); diff != "" {
|
||||
t.Fatal(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &QUICUDPLikeConn{
|
||||
MockSetDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetReadDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &QUICUDPLikeConn{
|
||||
MockSetReadDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetReadDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetWriteDeadline", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &QUICUDPLikeConn{
|
||||
MockSetWriteDeadline: func(t time.Time) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.SetWriteDeadline(time.Time{})
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ConnReadFrom", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
quc := &QUICUDPLikeConn{
|
||||
MockReadFrom: func(b []byte) (int, net.Addr, error) {
|
||||
return 0, nil, expected
|
||||
},
|
||||
}
|
||||
b := make([]byte, 128)
|
||||
n, addr, err := quc.ReadFrom(b)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatal("expected zero here")
|
||||
}
|
||||
if addr != nil {
|
||||
t.Fatal("expected nil here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SyscallConn", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
quc := &QUICUDPLikeConn{
|
||||
MockSyscallConn: func() (syscall.RawConn, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
conn, err := quc.SyscallConn()
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil here")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SetReadBuffer", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
quc := &QUICUDPLikeConn{
|
||||
MockSetReadBuffer: func(n int) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := quc.SetReadBuffer(1 << 10)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package mocks
|
||||
|
||||
import "io"
|
||||
|
||||
// Reader allows to mock any io.Reader.
|
||||
type Reader struct {
|
||||
MockRead func(b []byte) (int, error)
|
||||
}
|
||||
|
||||
// MockableReader implements an io.Reader.
|
||||
var _ io.Reader = &Reader{}
|
||||
|
||||
// Read implements io.Reader.Read.
|
||||
func (r *Reader) Read(b []byte) (int, error) {
|
||||
return r.MockRead(b)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
r := &Reader{
|
||||
MockRead: func(b []byte) (int, error) {
|
||||
return 0, expected
|
||||
},
|
||||
}
|
||||
b := make([]byte, 128)
|
||||
count, err := r.Read(b)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if count != 0 {
|
||||
t.Fatal("unexpected count", count)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
// Resolver is a mockable Resolver.
|
||||
type Resolver struct {
|
||||
MockLookupHost func(ctx context.Context, domain string) ([]string, error)
|
||||
MockNetwork func() string
|
||||
MockAddress func() string
|
||||
MockCloseIdleConnections func()
|
||||
MockLookupHTTPS func(ctx context.Context, domain string) (*model.HTTPSSvc, error)
|
||||
}
|
||||
|
||||
// LookupHost calls MockLookupHost.
|
||||
func (r *Resolver) LookupHost(ctx context.Context, domain string) ([]string, error) {
|
||||
return r.MockLookupHost(ctx, domain)
|
||||
}
|
||||
|
||||
// Address calls MockAddress.
|
||||
func (r *Resolver) Address() string {
|
||||
return r.MockAddress()
|
||||
}
|
||||
|
||||
// Network calls MockNetwork.
|
||||
func (r *Resolver) Network() string {
|
||||
return r.MockNetwork()
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (r *Resolver) CloseIdleConnections() {
|
||||
r.MockCloseIdleConnections()
|
||||
}
|
||||
|
||||
// LookupHTTPS calls MockLookupHTTPS.
|
||||
func (r *Resolver) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
||||
return r.MockLookupHTTPS(ctx, domain)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/model"
|
||||
)
|
||||
|
||||
func TestResolver(t *testing.T) {
|
||||
t.Run("LookupHost", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
r := &Resolver{
|
||||
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
addrs, err := r.LookupHost(ctx, "dns.google")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if addrs != nil {
|
||||
t.Fatal("expected nil addr")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Network", func(t *testing.T) {
|
||||
r := &Resolver{
|
||||
MockNetwork: func() string {
|
||||
return "antani"
|
||||
},
|
||||
}
|
||||
if v := r.Network(); v != "antani" {
|
||||
t.Fatal("unexpected network", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Address", func(t *testing.T) {
|
||||
r := &Resolver{
|
||||
MockAddress: func() string {
|
||||
return "1.1.1.1"
|
||||
},
|
||||
}
|
||||
if v := r.Address(); v != "1.1.1.1" {
|
||||
t.Fatal("unexpected address", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
var called bool
|
||||
r := &Resolver{
|
||||
MockCloseIdleConnections: func() {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
r.CloseIdleConnections()
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("LookupHTTPS", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
r := &Resolver{
|
||||
MockLookupHTTPS: func(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
https, err := r.LookupHTTPS(ctx, "dns.google")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if https != nil {
|
||||
t.Fatal("expected nil addr")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
// TLSHandshaker is a mockable TLS handshaker.
|
||||
type TLSHandshaker struct {
|
||||
MockHandshake func(ctx context.Context, conn net.Conn, config *tls.Config) (
|
||||
net.Conn, tls.ConnectionState, error)
|
||||
}
|
||||
|
||||
// Handshake calls MockHandshake.
|
||||
func (th *TLSHandshaker) Handshake(ctx context.Context, conn net.Conn, config *tls.Config) (
|
||||
net.Conn, tls.ConnectionState, error) {
|
||||
return th.MockHandshake(ctx, conn, config)
|
||||
}
|
||||
|
||||
// TLSConn allows to mock netxlite.TLSConn.
|
||||
type TLSConn struct {
|
||||
// Conn is the embedded mockable Conn.
|
||||
Conn
|
||||
|
||||
// MockConnectionState allows to mock the ConnectionState method.
|
||||
MockConnectionState func() tls.ConnectionState
|
||||
|
||||
// MockHandshakeContext allows to mock the HandshakeContext method.
|
||||
MockHandshakeContext func(ctx context.Context) error
|
||||
}
|
||||
|
||||
// ConnectionState calls MockConnectionState.
|
||||
func (c *TLSConn) ConnectionState() tls.ConnectionState {
|
||||
return c.MockConnectionState()
|
||||
}
|
||||
|
||||
// HandshakeContext calls MockHandshakeContext.
|
||||
func (c *TLSConn) HandshakeContext(ctx context.Context) error {
|
||||
return c.MockHandshakeContext(ctx)
|
||||
}
|
||||
|
||||
// TLSDialer allows to mock netxlite.TLSDialer.
|
||||
type TLSDialer struct {
|
||||
// MockCloseIdleConnections allows to mock the CloseIdleConnections method.
|
||||
MockCloseIdleConnections func()
|
||||
|
||||
// MockDialTLSContext allows to mock the DialTLSContext method.
|
||||
MockDialTLSContext func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// CloseIdleConnections calls MockCloseIdleConnections.
|
||||
func (d *TLSDialer) CloseIdleConnections() {
|
||||
d.MockCloseIdleConnections()
|
||||
}
|
||||
|
||||
// DialTLSContext calls MockDialTLSContext.
|
||||
func (d *TLSDialer) DialTLSContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return d.MockDialTLSContext(ctx, network, address)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTLSHandshaker(t *testing.T) {
|
||||
t.Run("Handshake", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
conn := &Conn{}
|
||||
ctx := context.Background()
|
||||
config := &tls.Config{}
|
||||
th := &TLSHandshaker{
|
||||
MockHandshake: func(ctx context.Context, conn net.Conn,
|
||||
config *tls.Config) (net.Conn, tls.ConnectionState, error) {
|
||||
return nil, tls.ConnectionState{}, expected
|
||||
},
|
||||
}
|
||||
tlsConn, connState, err := th.Handshake(ctx, conn, config)
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if !reflect.ValueOf(connState).IsZero() {
|
||||
t.Fatal("expected zero ConnectionState here")
|
||||
}
|
||||
if tlsConn != nil {
|
||||
t.Fatal("expected nil conn here")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTLSConn(t *testing.T) {
|
||||
t.Run("ConnectionState", func(t *testing.T) {
|
||||
state := tls.ConnectionState{Version: tls.VersionTLS12}
|
||||
c := &TLSConn{
|
||||
MockConnectionState: func() tls.ConnectionState {
|
||||
return state
|
||||
},
|
||||
}
|
||||
out := c.ConnectionState()
|
||||
if !reflect.DeepEqual(out, state) {
|
||||
t.Fatal("not the result we expected")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("HandshakeContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
c := &TLSConn{
|
||||
MockHandshakeContext: func(ctx context.Context) error {
|
||||
return expected
|
||||
},
|
||||
}
|
||||
err := c.HandshakeContext(context.Background())
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTLSDialer(t *testing.T) {
|
||||
t.Run("CloseIdleConnections", func(t *testing.T) {
|
||||
var called bool
|
||||
td := &TLSDialer{
|
||||
MockCloseIdleConnections: func() {
|
||||
called = true
|
||||
},
|
||||
}
|
||||
td.CloseIdleConnections()
|
||||
if !called {
|
||||
t.Fatal("not called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DialTLSContext", func(t *testing.T) {
|
||||
expected := errors.New("mocked error")
|
||||
td := &TLSDialer{
|
||||
MockDialTLSContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return nil, expected
|
||||
},
|
||||
}
|
||||
ctx := context.Background()
|
||||
conn, err := td.DialTLSContext(ctx, "", "")
|
||||
if !errors.Is(err, expected) {
|
||||
t.Fatal("not the error we expected", err)
|
||||
}
|
||||
if conn != nil {
|
||||
t.Fatal("expected nil conn here")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
)
|
||||
|
||||
//
|
||||
// Network extensions
|
||||
//
|
||||
|
||||
// The DNSDecoder decodes DNS replies.
|
||||
type DNSDecoder interface {
|
||||
// DecodeLookupHost decodes an A or AAAA reply.
|
||||
//
|
||||
// Arguments:
|
||||
//
|
||||
// - qtype is the query type (e.g., dns.TypeAAAA)
|
||||
//
|
||||
// - data contains the reply bytes read from a DNSTransport
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// - on success, a list of IP addrs inside the reply and a nil error
|
||||
//
|
||||
// - on failure, a nil list and an error.
|
||||
//
|
||||
// Note that this function will return an error if there is no
|
||||
// IP address inside of the reply.
|
||||
DecodeLookupHost(qtype uint16, data []byte) ([]string, error)
|
||||
|
||||
// DecodeHTTPS decodes an HTTPS reply.
|
||||
//
|
||||
// The argument is the reply as read by the DNSTransport.
|
||||
//
|
||||
// On success, this function returns an HTTPSSvc structure and
|
||||
// a nil error. On failure, the HTTPSSvc pointer is nil and
|
||||
// the error points to the error that occurred.
|
||||
//
|
||||
// This function will return an error if the HTTPS reply does not
|
||||
// contain at least a valid ALPN entry. It will not return
|
||||
// an error, though, when there are no IPv4/IPv6 hints in the reply.
|
||||
DecodeHTTPS(data []byte) (*HTTPSSvc, error)
|
||||
}
|
||||
|
||||
// The DNSEncoder encodes DNS queries to bytes
|
||||
type DNSEncoder interface {
|
||||
// Encode transforms its arguments into a serialized DNS query.
|
||||
//
|
||||
// Arguments:
|
||||
//
|
||||
// - domain is the domain for the query (e.g., x.org);
|
||||
//
|
||||
// - qtype is the query type (e.g., dns.TypeA);
|
||||
//
|
||||
// - padding is whether to add padding to the query.
|
||||
//
|
||||
// On success, this function returns a valid byte array and
|
||||
// a nil error. On failure, we have an error and the byte array is nil.
|
||||
Encode(domain string, qtype uint16, padding bool) ([]byte, error)
|
||||
}
|
||||
|
||||
// DNSTransport represents an abstract DNS transport.
|
||||
type DNSTransport interface {
|
||||
// RoundTrip sends a DNS query and receives the reply.
|
||||
RoundTrip(ctx context.Context, query []byte) (reply []byte, err error)
|
||||
|
||||
// RequiresPadding returns whether this transport needs padding.
|
||||
RequiresPadding() bool
|
||||
|
||||
// Network is the network of the round tripper (e.g. "dot").
|
||||
Network() string
|
||||
|
||||
// Address is the address of the round tripper (e.g. "1.1.1.1:853").
|
||||
Address() string
|
||||
|
||||
// CloseIdleConnections closes idle connections, if any.
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// SimpleDialer establishes network connections.
|
||||
type SimpleDialer interface {
|
||||
// DialContext behaves like net.Dialer.DialContext.
|
||||
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// Dialer is a SimpleDialer with the possibility of closing open connections.
|
||||
type Dialer interface {
|
||||
// A Dialer is also a SimpleDialer.
|
||||
SimpleDialer
|
||||
|
||||
// CloseIdleConnections closes idle connections, if any.
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// HTTPClient is an http.Client-like interface.
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// HTTPTransport is an http.Transport-like structure.
|
||||
type HTTPTransport interface {
|
||||
// RoundTrip performs the HTTP round trip.
|
||||
RoundTrip(req *http.Request) (*http.Response, error)
|
||||
|
||||
// CloseIdleConnections closes idle connections.
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// HTTPSSvc is the reply to an HTTPS DNS query.
|
||||
type HTTPSSvc struct {
|
||||
// ALPN contains the ALPNs inside the HTTPS reply.
|
||||
ALPN []string
|
||||
|
||||
// IPv4 contains the IPv4 hints (which may be empty).
|
||||
IPv4 []string
|
||||
|
||||
// IPv6 contains the IPv6 hints (which may be empty).
|
||||
IPv6 []string
|
||||
}
|
||||
|
||||
// QUICListener listens for QUIC connections.
|
||||
type QUICListener interface {
|
||||
// Listen creates a new listening UDPLikeConn.
|
||||
Listen(addr *net.UDPAddr) (UDPLikeConn, error)
|
||||
}
|
||||
|
||||
// QUICDialer dials QUIC sessions.
|
||||
type QUICDialer interface {
|
||||
// DialContext establishes a new QUIC session using the given
|
||||
// network and address. The tlsConfig and the quicConfig arguments
|
||||
// MUST NOT be nil. Returns either the session or an error.
|
||||
//
|
||||
// Recommended tlsConfig setup:
|
||||
//
|
||||
// - set ServerName to be the SNI;
|
||||
//
|
||||
// - set RootCAs to NewDefaultCertPool();
|
||||
//
|
||||
// - set NextProtos to []string{"h3"}.
|
||||
//
|
||||
// Typically, you want to pass `&quic.Config{}` as quicConfig.
|
||||
DialContext(ctx context.Context, network, address string,
|
||||
tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlySession, error)
|
||||
|
||||
// CloseIdleConnections closes idle connections, if any.
|
||||
CloseIdleConnections()
|
||||
}
|
||||
|
||||
// Resolver performs domain name resolutions.
|
||||
type Resolver interface {
|
||||
// LookupHost behaves like net.Resolver.LookupHost.
|
||||
LookupHost(ctx context.Context, hostname string) (addrs []string, err error)
|
||||
|
||||
// Network returns the resolver type (e.g., system, dot, doh).
|
||||
Network() string
|
||||
|
||||
// Address returns the resolver address (e.g., 8.8.8.8:53).
|
||||
Address() string
|
||||
|
||||
// CloseIdleConnections closes idle connections, if any.
|
||||
CloseIdleConnections()
|
||||
|
||||
// LookupHTTPS issues an HTTPS query for a domain.
|
||||
LookupHTTPS(
|
||||
ctx context.Context, domain string) (*HTTPSSvc, error)
|
||||
}
|
||||
|
||||
// TLSDialer is a Dialer dialing TLS connections.
|
||||
type TLSDialer interface {
|
||||
// CloseIdleConnections closes idle connections, if any.
|
||||
CloseIdleConnections()
|
||||
|
||||
// DialTLSContext dials a TLS connection. This method will always return
|
||||
// to you a oohttp.TLSConn, so you can always safely cast to it.
|
||||
DialTLSContext(ctx context.Context, network, address string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// TLSHandshaker is the generic TLS handshaker.
|
||||
type TLSHandshaker interface {
|
||||
// Handshake creates a new TLS connection from the given connection and
|
||||
// the given config. This function DOES NOT take ownership of the connection
|
||||
// and it's your responsibility to close it on failure.
|
||||
//
|
||||
// Recommended tlsConfig setup:
|
||||
//
|
||||
// - set ServerName to be the SNI;
|
||||
//
|
||||
// - set RootCAs to NewDefaultCertPool();
|
||||
//
|
||||
// - set NextProtos to []string{"h2", "http/1.1"} for HTTPS
|
||||
// and []string{"dot"} for DNS-over-TLS.
|
||||
//
|
||||
// QUIRK: The returned connection will always implement the TLSConn interface
|
||||
// exposed by ooni/oohttp. A future version of this interface may instead
|
||||
// return directly a TLSConn to avoid unconditional castings.
|
||||
Handshake(ctx context.Context, conn net.Conn, tlsConfig *tls.Config) (
|
||||
net.Conn, tls.ConnectionState, error)
|
||||
}
|
||||
|
||||
// UDPLikeConn is a net.PacketConn with some extra functions
|
||||
// required to convince the QUIC library (lucas-clemente/quic-go)
|
||||
// to inflate the receive buffer of the connection.
|
||||
//
|
||||
// The QUIC library will treat this connection as a "dumb"
|
||||
// net.PacketConn, calling its ReadFrom and WriteTo methods
|
||||
// as opposed to more efficient methods that are available
|
||||
// under Linux and (maybe?) FreeBSD.
|
||||
//
|
||||
// It seems fine to avoid performance optimizations, because
|
||||
// they would complicate the implementation on our side and
|
||||
// our use cases (blocking and heavy throttling) do not seem
|
||||
// to require such optimizations.
|
||||
//
|
||||
// See https://github.com/ooni/probe/issues/1754 for a more
|
||||
// comprehensive discussion of UDPLikeConn.
|
||||
type UDPLikeConn interface {
|
||||
// An UDPLikeConn is a net.PacketConn conn.
|
||||
net.PacketConn
|
||||
|
||||
// SetReadBuffer allows setting the read buffer.
|
||||
SetReadBuffer(bytes int) error
|
||||
|
||||
// SyscallConn returns a conn suitable for calling syscalls,
|
||||
// which is also instrumental to setting the read buffer.
|
||||
SyscallConn() (syscall.RawConn, error)
|
||||
}
|
||||
|
||||
// UnderlyingNetworkLibrary defines the basic functionality from
|
||||
// which the network extensions depend. By changing the default
|
||||
// implementation of this interface, we can implement a wide array
|
||||
// of tests, including self censorship tests.
|
||||
type UnderlyingNetworkLibrary interface {
|
||||
// ListenUDP creates a new model.UDPLikeConn conn.
|
||||
ListenUDP(network string, laddr *net.UDPAddr) (UDPLikeConn, error)
|
||||
|
||||
// LookupHost lookups a domain using the stdlib resolver.
|
||||
LookupHost(ctx context.Context, domain string) ([]string, error)
|
||||
|
||||
// NewSimpleDialer returns a new SimpleDialer.
|
||||
NewSimpleDialer(timeout time.Duration) SimpleDialer
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package model
|
||||
|
||||
//
|
||||
// Data structures used to speak with the OONI API.
|
||||
//
|
||||
|
||||
// OOAPICheckInConfigWebConnectivity is the configuration for the WebConnectivity test
|
||||
type OOAPICheckInConfigWebConnectivity struct {
|
||||
CategoryCodes []string `json:"category_codes"` // CategoryCodes is an array of category codes
|
||||
}
|
||||
|
||||
// OOAPICheckInConfig contains configuration for calling the checkin API.
|
||||
type OOAPICheckInConfig struct {
|
||||
Charging bool `json:"charging"` // Charging indicate if the phone is actually charging
|
||||
OnWiFi bool `json:"on_wifi"` // OnWiFi indicate if the phone is actually connected to a WiFi network
|
||||
Platform string `json:"platform"` // Platform of the probe
|
||||
ProbeASN string `json:"probe_asn"` // ProbeASN is the probe country code
|
||||
ProbeCC string `json:"probe_cc"` // ProbeCC is the probe country code
|
||||
RunType string `json:"run_type"` // RunType
|
||||
SoftwareName string `json:"software_name"` // SoftwareName of the probe
|
||||
SoftwareVersion string `json:"software_version"` // SoftwareVersion of the probe
|
||||
WebConnectivity OOAPICheckInConfigWebConnectivity `json:"web_connectivity"` // WebConnectivity class contain an array of categories
|
||||
}
|
||||
|
||||
// OOAPICheckInInfoWebConnectivity contains the array of URLs returned by the checkin API
|
||||
type OOAPICheckInInfoWebConnectivity struct {
|
||||
ReportID string `json:"report_id"`
|
||||
URLs []OOAPIURLInfo `json:"urls"`
|
||||
}
|
||||
|
||||
// OOAPICheckInInfo contains the return test objects from the checkin API
|
||||
type OOAPICheckInInfo struct {
|
||||
WebConnectivity *OOAPICheckInInfoWebConnectivity `json:"web_connectivity"`
|
||||
}
|
||||
|
||||
// OOAPIService describes a backend service.
|
||||
//
|
||||
// The fields of this struct have the meaning described in v2.0.0 of the OONI
|
||||
// bouncer specification defined by
|
||||
// https://github.com/ooni/spec/blob/master/backends/bk-004-bouncer.md.
|
||||
type OOAPIService struct {
|
||||
// Address is the address of the server.
|
||||
Address string `json:"address"`
|
||||
|
||||
// Type is the type of the service.
|
||||
Type string `json:"type"`
|
||||
|
||||
// Front is the front to use with "cloudfront" type entries.
|
||||
Front string `json:"front,omitempty"`
|
||||
}
|
||||
|
||||
// OOAPITorTarget is a target for the tor experiment.
|
||||
type OOAPITorTarget struct {
|
||||
// Address is the address of the target.
|
||||
Address string `json:"address"`
|
||||
|
||||
// Name is the name of the target.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Params contains optional params for, e.g., pluggable transports.
|
||||
Params map[string][]string `json:"params"`
|
||||
|
||||
// Protocol is the protocol to use with the target.
|
||||
Protocol string `json:"protocol"`
|
||||
|
||||
// Source is the source from which we fetched this specific
|
||||
// target. Whenever the source is non-empty, we will treat
|
||||
// this specific target as a private target.
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
// OOAPIURLInfo contains info on a test lists URL
|
||||
type OOAPIURLInfo struct {
|
||||
CategoryCode string `json:"category_code"`
|
||||
CountryCode string `json:"country_code"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// OOAPIURLListConfig contains configuration for fetching the URL list.
|
||||
type OOAPIURLListConfig struct {
|
||||
Categories []string // Categories to query for (empty means all)
|
||||
CountryCode string // CountryCode is the optional country code
|
||||
Limit int64 // Max number of URLs (<= 0 means no limit)
|
||||
}
|
||||
Reference in New Issue
Block a user