Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(testingx): add OONI collector for testing #1574

Merged
merged 45 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
75ef7fd
refactor: consolidate httpx and httpapi
bassosimone Apr 22, 2024
f9210ec
refactor to make testing the whole package easier
bassosimone Apr 23, 2024
587290c
Merge branch 'master' into issue/2700
bassosimone Apr 23, 2024
af394c2
Merge branch 'master' into issue/2700
bassosimone Apr 23, 2024
c6f2f5a
Merge branch 'master' into issue/2700
bassosimone Apr 23, 2024
68c9779
Merge branch 'issue/2700' of github.com:ooni/probe-cli into issue/2700
bassosimone Apr 23, 2024
57e29da
Merge branch 'master' into issue/2700
bassosimone Apr 23, 2024
5c953f0
x
bassosimone Apr 23, 2024
e03e810
x
bassosimone Apr 23, 2024
a6046fd
x
bassosimone Apr 23, 2024
341fcf2
x
bassosimone Apr 23, 2024
8c34524
x
bassosimone Apr 23, 2024
4b464ff
try to entirely remove httpx usages
bassosimone Apr 23, 2024
6d57184
fix: make sure there is nil safety
bassosimone Apr 23, 2024
9c2a226
oxford comma: yes/no?
bassosimone Apr 23, 2024
1123b4e
x
bassosimone Apr 23, 2024
d421d24
fix: unit test needs to be adapted
bassosimone Apr 24, 2024
67e0a10
chore: improve testing for cloudflare IP lookup
bassosimone Apr 24, 2024
a69d981
chore: improve the ubuntu IP lookup tests
bassosimone Apr 24, 2024
cd25c56
Merge branch 'master' into issue/2700
bassosimone Apr 24, 2024
642ae5c
x
bassosimone Apr 24, 2024
548e6bc
doc: document oonirun/v2_test.go tests
bassosimone Apr 24, 2024
40db0e5
Merge branch 'master' into issue/2700
bassosimone Apr 24, 2024
4cf3566
start improving probeservices tests
bassosimone Apr 24, 2024
e736e42
Merge branch 'master' into issue/2700
bassosimone Apr 26, 2024
e8471c4
x
bassosimone Apr 26, 2024
aa1c836
Merge branch 'master' into issue/2700
bassosimone Apr 26, 2024
08e81a9
x
bassosimone Apr 26, 2024
fa74b48
x
bassosimone Apr 26, 2024
a7e748f
x
bassosimone Apr 26, 2024
87146cc
x
bassosimone Apr 26, 2024
dac7b8f
x
bassosimone Apr 26, 2024
04b0071
Merge branch 'master' into issue/2700
bassosimone Apr 26, 2024
79d1fee
Merge branch 'master' into issue/2700
bassosimone Apr 29, 2024
88b399d
Merge branch 'master' into issue/2700
bassosimone Apr 29, 2024
de23e7d
x
bassosimone Apr 29, 2024
9d87673
Merge branch 'master' into issue/2700
bassosimone Apr 29, 2024
a436f1e
x
bassosimone Apr 29, 2024
08f8ca9
Merge branch 'master' into issue/2700
bassosimone Apr 29, 2024
25140f3
x
bassosimone Apr 29, 2024
1bbe0b7
chore: write tests for oonicollector.go
bassosimone Apr 30, 2024
e4fde0a
x
bassosimone Apr 30, 2024
38527c3
x
bassosimone Apr 30, 2024
12f8a04
x
bassosimone Apr 30, 2024
a631cc1
x
bassosimone Apr 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/testingx/oonibackendwithlogin.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ type OONIBackendWithLoginFlowUserRecord struct {
Token string
}

// OONIBackendWithLoginFlow is an [http.Handler] that implements the register and
// loging workflow and serves psiphon and tor config.
// OONIBackendWithLoginFlow implements the register and login workflows
// and serves the psiphon config and tor targets.
//
// The zero value is ready to use.
//
Expand Down
245 changes: 245 additions & 0 deletions internal/testingx/oonicollector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package testingx

import (
"encoding/json"
"io"
"log"
"net/http"
"strings"
"sync"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/must"
"github.com/ooni/probe-cli/v3/internal/runtimex"
)

// OONICollector implements the OONI collector for testing.
//
// The zero value is ready to use.
//
// This struct methods panics for several errors. Only use for testing purposes!
type OONICollector struct {
// EditOpenReportResponse is an OPTIONAL callback to edit the response
// before the server actually sends it to the client.
EditOpenReportResponse func(resp *model.OOAPICollectorOpenResponse)

// EditUpdateResponse is an OPTIONAL callback to edit the response
// before the server actually sends it to the client.
EditUpdateResponse func(resp *model.OOAPICollectorUpdateResponse)

// ValidateMeasurement is an OPTIONAL callback to validate the incoming measurement
// beyond checks that ensure it is consistent with the original template.
ValidateMeasurement func(meas *model.Measurement) error

// ValidateReportTemplate is an OPTIONAL callback to validate the incoming report
// template beyond the data format version and format fields values.
ValidateReportTemplate func(rt *model.OOAPIReportTemplate) error

// mu provides mutual exclusion.
mu sync.Mutex

// reports contains the open reports.
reports map[string]*model.OOAPIReportTemplate
}

// OpenReport opens a report for the given report ID and template.
//
// This method is safe to call concurrently with other methods.
func (oc *OONICollector) OpenReport(reportID string, template *model.OOAPIReportTemplate) {
oc.mu.Lock()
if oc.reports == nil {
oc.reports = make(map[string]*model.OOAPIReportTemplate)
}
oc.reports[reportID] = template
oc.mu.Unlock()
}

// ServeHTTP implements [http.Handler].
//
// This method is safe to call concurrently with other methods.
func (oc *OONICollector) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// make sure that the method is POST
if r.Method != "POST" {
log.Printf("OONICollector: invalid method")
w.WriteHeader(http.StatusNotImplemented)
return
}

// make sure the URL path starts with /report
if !strings.HasPrefix(r.URL.Path, "/report") {
log.Printf("OONICollector: invalid URL path prefix")
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure that the content-type is application/json
if r.Header.Get("Content-Type") != "application/json" {
log.Printf("OONICollector: missing content-type header")
w.WriteHeader(http.StatusBadRequest)
return
}

// read the raw request body or panic if we cannot read it
body := runtimex.Try1(io.ReadAll(r.Body))

log.Printf("OONICollector: URLPath %+v", r.URL.Path)
log.Printf("OONICollector: request body %s", string(body))

// handle the case where the user wants to open a new report
if r.URL.Path == "/report" {
log.Printf("OONICollector: opening new report")
oc.openReport(w, body)
return
}

// handle the case where the user wants to append to an existing report
log.Printf("OONICollector: updating existing report")
oc.updateReport(w, r.URL.Path, body)
}

// openReport handles opening a new OONI report.
func (oc *OONICollector) openReport(w http.ResponseWriter, body []byte) {
// make sure we can parse the incoming request
var template model.OOAPIReportTemplate
if err := json.Unmarshal(body, &template); err != nil {
log.Printf("OONICollector: cannot unmarshal JSON: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure the data format version is OK
if template.DataFormatVersion != model.OOAPIReportDefaultDataFormatVersion {
log.Printf("OONICollector: invalid data format version")
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure the format is also OK
if template.Format != model.OOAPIReportDefaultFormat {
log.Printf("OONICollector: invalid format")
w.WriteHeader(http.StatusBadRequest)
return
}

// optionally allow the user to validate the report template
if oc.ValidateReportTemplate != nil {
if err := oc.ValidateReportTemplate(&template); err != nil {
log.Printf("OONICollector: invalid report template: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
}

// create the response
response := &model.OOAPICollectorOpenResponse{
BackendVersion: "1.3.0",
ReportID: uuid.Must(uuid.NewRandom()).String(),
SupportedFormats: []string{
model.OOAPIReportDefaultFormat,
},
}

// optionally allow the user to modify the response
if oc.EditOpenReportResponse != nil {
oc.EditOpenReportResponse(response)
}

// make sure we know that this report ID now exists - note that this must
// happen after the client code has edited the response
oc.OpenReport(response.ReportID, &template)

// set the content-type header
w.Header().Set("Content-Type", "application/json")

// serialize and send
w.Write(must.MarshalJSON(response))
}

// updateReport handles updating an existing OONI report.
func (oc *OONICollector) updateReport(w http.ResponseWriter, urlpath string, body []byte) {
// get the report ID
reportID := strings.TrimPrefix(urlpath, "/report/")

// obtain the report template
oc.mu.Lock()
template := oc.reports[reportID]
oc.mu.Unlock()

// handle the case of missing template
if template == nil {
log.Printf("OONICollector: the report does not exist: %s", reportID)
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure we can parse the incoming request
var request model.OOAPICollectorUpdateRequest
if err := json.Unmarshal(body, &request); err != nil {
log.Printf("OONICollector: cannot unmarshal JSON: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure the measurement is encoded as JSON
if request.Format != "json" {
log.Printf("OONICollector: invalid request format: %s", request.Format)
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure we can parse the content
//
// note: we unmarshaled into a map[string]any so we need to marshal
// and unmarshal again to get a measurement structure
var measurement model.Measurement
if err := json.Unmarshal(must.MarshalJSON(request.Content), &measurement); err != nil {
log.Printf("OONICollector: cannot unmarshal JSON: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}

// make sure all the required fields match
mt := &model.OOAPIReportTemplate{
DataFormatVersion: measurement.DataFormatVersion,
Format: request.Format,
ProbeASN: measurement.ProbeASN,
ProbeCC: measurement.ProbeCC,
SoftwareName: measurement.SoftwareName,
SoftwareVersion: measurement.SoftwareVersion,
TestName: measurement.TestName,
TestStartTime: measurement.TestStartTime,
TestVersion: measurement.TestVersion,
}
if diff := cmp.Diff(template, mt); diff != "" {
log.Printf("OONICollector: measurement differs from template %s", diff)
w.WriteHeader(http.StatusBadRequest)
return
}

// give the user a chance to validate the measurement
if oc.ValidateMeasurement != nil {
if err := oc.ValidateMeasurement(&measurement); err != nil {
log.Printf("OONICollector: invalid measurement: %s", err.Error())
w.WriteHeader(http.StatusBadRequest)
return
}
}

// create the response
response := &model.OOAPICollectorUpdateResponse{
MeasurementUID: uuid.Must(uuid.NewRandom()).String(),
}

// optionally allow the user to modify the response
if oc.EditUpdateResponse != nil {
oc.EditUpdateResponse(response)
}

// set the content-type header
w.Header().Set("Content-Type", "application/json")

// serialize and send
w.Write(must.MarshalJSON(response))
}
Loading
Loading