From ddd64f21af4a01b47e001c1586af0014906780c5 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Thu, 28 Sep 2023 17:07:41 +0200 Subject: [PATCH] chore: write tests for ArchivalHTTPRequestResult (#1328) My aim here is to have marshal/unmarshal tests for all the toplevel data structures inside the internal/model/archival.go file. Once I have done that, I can have additional confidence about changing the structure fields and simplifying the code. All of this is preparatory work to automatically scrub HTTP measurements which is something we should really do before continuing to improve the boostrap process in light of https://github.com/ooni/probe/issues/2531. --- internal/model/archival_test.go | 331 +++++++++++++++++++++++++++++++- 1 file changed, 330 insertions(+), 1 deletion(-) diff --git a/internal/model/archival_test.go b/internal/model/archival_test.go index ff2191201c..6665a91240 100644 --- a/internal/model/archival_test.go +++ b/internal/model/archival_test.go @@ -1434,7 +1434,336 @@ func TestArchivalTLSOrQUICHandshakeResult(t *testing.T) { // This test ensures that ArchivalHTTPRequestResult is WAI func TestArchivalHTTPRequestResult(t *testing.T) { - t.Skip("not implemented") + + // This test ensures that we correctly serialize to JSON. + t.Run("MarshalJSON", func(t *testing.T) { + // testcase is a test case defined by this function + type testcase struct { + // name is the name of the test case + name string + + // input is the input struct + input model.ArchivalHTTPRequestResult + + // expectErr is the error we expect to see or nil + expectErr error + + // expectData is the data we expect to see + expectData []byte + } + + cases := []testcase{{ + name: "serialization of a successful HTTP request", + input: model.ArchivalHTTPRequestResult{ + Network: "tcp", + Address: "[2606:2800:220:1:248:1893:25c8:1946]:443", + ALPN: "h2", + Failure: nil, + Request: model.ArchivalHTTPRequest{ + Body: model.ArchivalMaybeBinaryData{Value: ""}, + BodyIsTruncated: false, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Accept", + Value: model.ArchivalMaybeBinaryData{ + Value: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + }, { + Key: "User-Agent", + Value: model.ArchivalMaybeBinaryData{ + Value: "miniooni/0.1.0", + }, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, + "User-Agent": {"miniooni/0.1.0"}, + }, + Method: "GET", + Tor: model.ArchivalHTTPTor{ + ExitIP: nil, + ExitName: nil, + IsTor: false, + }, + Transport: "tcp", + URL: "https://www.example.com/", + }, + Response: model.ArchivalHTTPResponse{ + Body: model.ArchivalMaybeBinaryData{ + Value: "Bonsoir, Elliot!", + }, + BodyIsTruncated: false, + Code: 200, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Age", + Value: model.ArchivalMaybeBinaryData{"131833"}, + }, { + Key: "Server", + Value: model.ArchivalMaybeBinaryData{"Apache"}, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Age": {"131833"}, + "Server": {"Apache"}, + }, + Locations: nil, + }, + T0: 0.7, + T: 1.33, + Tags: []string{"http"}, + TransactionID: 5, + }, + expectErr: nil, + expectData: []byte(`{"network":"tcp","address":"[2606:2800:220:1:248:1893:25c8:1946]:443","alpn":"h2","failure":null,"request":{"body":"","body_is_truncated":false,"headers_list":[["Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],["User-Agent","miniooni/0.1.0"]],"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","User-Agent":"miniooni/0.1.0"},"method":"GET","tor":{"exit_ip":null,"exit_name":null,"is_tor":false},"x_transport":"tcp","url":"https://www.example.com/"},"response":{"body":"Bonsoir, Elliot!","body_is_truncated":false,"code":200,"headers_list":[["Age","131833"],["Server","Apache"]],"headers":{"Age":"131833","Server":"Apache"}},"t0":0.7,"t":1.33,"tags":["http"],"transaction_id":5}`), + }, { + name: "serialization of a failed HTTP request", + input: model.ArchivalHTTPRequestResult{ + Network: "tcp", + Address: "[2606:2800:220:1:248:1893:25c8:1946]:443", + ALPN: "h2", + Failure: (func() *string { + s := netxlite.FailureGenericTimeoutError + return &s + })(), + Request: model.ArchivalHTTPRequest{ + Body: model.ArchivalMaybeBinaryData{Value: ""}, + BodyIsTruncated: false, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Accept", + Value: model.ArchivalMaybeBinaryData{ + Value: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + }, { + Key: "User-Agent", + Value: model.ArchivalMaybeBinaryData{ + Value: "miniooni/0.1.0", + }, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, + "User-Agent": {"miniooni/0.1.0"}, + }, + Method: "GET", + Tor: model.ArchivalHTTPTor{ + ExitIP: nil, + ExitName: nil, + IsTor: false, + }, + Transport: "tcp", + URL: "https://www.example.com/", + }, + Response: model.ArchivalHTTPResponse{ + Body: model.ArchivalMaybeBinaryData{}, + BodyIsTruncated: false, + Code: 0, + HeadersList: []model.ArchivalHTTPHeader{}, + Headers: map[string]model.ArchivalMaybeBinaryData{}, + Locations: nil, + }, + T0: 0.4, + T: 1.563, + Tags: []string{"http"}, + TransactionID: 6, + }, + expectErr: nil, + expectData: []byte(`{"network":"tcp","address":"[2606:2800:220:1:248:1893:25c8:1946]:443","alpn":"h2","failure":"generic_timeout_error","request":{"body":"","body_is_truncated":false,"headers_list":[["Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],["User-Agent","miniooni/0.1.0"]],"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","User-Agent":"miniooni/0.1.0"},"method":"GET","tor":{"exit_ip":null,"exit_name":null,"is_tor":false},"x_transport":"tcp","url":"https://www.example.com/"},"response":{"body":"","body_is_truncated":false,"code":0,"headers_list":[],"headers":{}},"t0":0.4,"t":1.563,"tags":["http"],"transaction_id":6}`), + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // serialize to JSON + data, err := json.Marshal(tc.input) + + t.Log("got this error", err) + t.Log("got this raw data", data) + t.Logf("converted to string: %s", string(data)) + + // handle errors + switch { + case err == nil && tc.expectErr != nil: + t.Fatal("expected", tc.expectErr, "got", err) + + case err != nil && tc.expectErr == nil: + t.Fatal("expected", tc.expectErr, "got", err) + + case err != nil && tc.expectErr != nil: + if err.Error() != tc.expectErr.Error() { + t.Fatal("expected", tc.expectErr, "got", err) + } + + case err == nil && tc.expectErr == nil: + // all good--fallthrough + } + + // make sure the serialization is OK + if diff := cmp.Diff(tc.expectData, data); diff != "" { + t.Fatal(diff) + } + }) + } + }) + + // This test ensures that we can unmarshal from the JSON representation + t.Run("UnmarshalJSON", func(t *testing.T) { + // testcase is a test case defined by this function + type testcase struct { + // name is the name of the test case + name string + + // input is the binary input + input []byte + + // expectErr is the error we expect to see or nil + expectErr error + + // expectStruct is the struct we expect to see + expectStruct model.ArchivalHTTPRequestResult + } + + cases := []testcase{{ + name: "deserialization of a successful HTTP request", + expectErr: nil, + input: []byte(`{"network":"tcp","address":"[2606:2800:220:1:248:1893:25c8:1946]:443","alpn":"h2","failure":null,"request":{"body":"","body_is_truncated":false,"headers_list":[["Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],["User-Agent","miniooni/0.1.0"]],"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","User-Agent":"miniooni/0.1.0"},"method":"GET","tor":{"exit_ip":null,"exit_name":null,"is_tor":false},"x_transport":"tcp","url":"https://www.example.com/"},"response":{"body":"Bonsoir, Elliot!","body_is_truncated":false,"code":200,"headers_list":[["Age","131833"],["Server","Apache"]],"headers":{"Age":"131833","Server":"Apache"}},"t0":0.7,"t":1.33,"tags":["http"],"transaction_id":5}`), + expectStruct: model.ArchivalHTTPRequestResult{ + Network: "tcp", + Address: "[2606:2800:220:1:248:1893:25c8:1946]:443", + ALPN: "h2", + Failure: nil, + Request: model.ArchivalHTTPRequest{ + Body: model.ArchivalMaybeBinaryData{Value: ""}, + BodyIsTruncated: false, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Accept", + Value: model.ArchivalMaybeBinaryData{ + Value: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + }, { + Key: "User-Agent", + Value: model.ArchivalMaybeBinaryData{ + Value: "miniooni/0.1.0", + }, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, + "User-Agent": {"miniooni/0.1.0"}, + }, + Method: "GET", + Tor: model.ArchivalHTTPTor{ + ExitIP: nil, + ExitName: nil, + IsTor: false, + }, + Transport: "tcp", + URL: "https://www.example.com/", + }, + Response: model.ArchivalHTTPResponse{ + Body: model.ArchivalMaybeBinaryData{ + Value: "Bonsoir, Elliot!", + }, + BodyIsTruncated: false, + Code: 200, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Age", + Value: model.ArchivalMaybeBinaryData{"131833"}, + }, { + Key: "Server", + Value: model.ArchivalMaybeBinaryData{"Apache"}, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Age": {"131833"}, + "Server": {"Apache"}, + }, + Locations: nil, + }, + T0: 0.7, + T: 1.33, + Tags: []string{"http"}, + TransactionID: 5, + }, + }, { + name: "deserialization of a failed HTTP request", + input: []byte(`{"network":"tcp","address":"[2606:2800:220:1:248:1893:25c8:1946]:443","alpn":"h2","failure":"generic_timeout_error","request":{"body":"","body_is_truncated":false,"headers_list":[["Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"],["User-Agent","miniooni/0.1.0"]],"headers":{"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","User-Agent":"miniooni/0.1.0"},"method":"GET","tor":{"exit_ip":null,"exit_name":null,"is_tor":false},"x_transport":"tcp","url":"https://www.example.com/"},"response":{"body":"","body_is_truncated":false,"code":0,"headers_list":[],"headers":{}},"t0":0.4,"t":1.563,"tags":["http"],"transaction_id":6}`), + expectErr: nil, + expectStruct: model.ArchivalHTTPRequestResult{ + Network: "tcp", + Address: "[2606:2800:220:1:248:1893:25c8:1946]:443", + ALPN: "h2", + Failure: (func() *string { + s := netxlite.FailureGenericTimeoutError + return &s + })(), + Request: model.ArchivalHTTPRequest{ + Body: model.ArchivalMaybeBinaryData{Value: ""}, + BodyIsTruncated: false, + HeadersList: []model.ArchivalHTTPHeader{{ + Key: "Accept", + Value: model.ArchivalMaybeBinaryData{ + Value: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + }, + }, { + Key: "User-Agent", + Value: model.ArchivalMaybeBinaryData{ + Value: "miniooni/0.1.0", + }, + }}, + Headers: map[string]model.ArchivalMaybeBinaryData{ + "Accept": {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, + "User-Agent": {"miniooni/0.1.0"}, + }, + Method: "GET", + Tor: model.ArchivalHTTPTor{ + ExitIP: nil, + ExitName: nil, + IsTor: false, + }, + Transport: "tcp", + URL: "https://www.example.com/", + }, + Response: model.ArchivalHTTPResponse{ + Body: model.ArchivalMaybeBinaryData{}, + BodyIsTruncated: false, + Code: 0, + HeadersList: []model.ArchivalHTTPHeader{}, + Headers: map[string]model.ArchivalMaybeBinaryData{}, + Locations: nil, + }, + T0: 0.4, + T: 1.563, + Tags: []string{"http"}, + TransactionID: 6, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // parse the JSON + var data model.ArchivalHTTPRequestResult + err := json.Unmarshal(tc.input, &data) + + t.Log("got this error", err) + t.Logf("got this struct %+v", data) + + // handle errors + switch { + case err == nil && tc.expectErr != nil: + t.Fatal("expected", tc.expectErr, "got", err) + + case err != nil && tc.expectErr == nil: + t.Fatal("expected", tc.expectErr, "got", err) + + case err != nil && tc.expectErr != nil: + if err.Error() != tc.expectErr.Error() { + t.Fatal("expected", tc.expectErr, "got", err) + } + + case err == nil && tc.expectErr == nil: + // all good--fallthrough + } + + // make sure the deserialization is OK + if diff := cmp.Diff(tc.expectStruct, data); diff != "" { + t.Fatal(diff) + } + }) + } + }) } // This test ensures that ArchivalNetworkEvent is WAI