diff --git a/QA/index.mjs b/QA/index.mjs deleted file mode 100644 index d57c761..0000000 --- a/QA/index.mjs +++ /dev/null @@ -1,106 +0,0 @@ -// This script performs QA checks. - -import { runTestCase } from "./lib/runner.mjs" -import { testCases as webTestCases } from "./lib/web.mjs" - -// testCases lists all the test cases. -// -// A test case is an object with the structure described -// by the documentation of runTestCase in ./lib/runner.mjs. -const testCases = [ - ...webTestCases, -] - -// checkForDuplicateTestCases ensures there are no duplicate names. -function checkForDuplicateTestCases() { - let dups = {} - for (let i = 0; i < testCases.length; i++) { - const testCase = testCases[i] - if (dups[testCase.name] !== undefined) { - console.log(`fatal: duplicate test case name: ${testCase.name}`) - process.exit(1) - } - dups[testCase.name] = true - } -} - -// runAllTestCases runs all the available test cases. -function runAllTestCases() { - let result = true - for (let i = 0; i < testCases.length; i++) { - result = result && runTestCase(testCases[i]) - } - return result -} - -// makeTestCasesMap creates a map from the test case name -// to the test case definition. -function makeTestCasesMap() { - var map = {} - for (let i = 0; i < testCases.length; i++) { - const testCase = testCases[i] - map[testCase.name] = testCase - } - return map -} - -// commandRun implements the run command. -function commandRun(args) { - const bailOnFailure = (result) => { - if (!result) { - console.log("some checks failed (see above logs)") - process.exit(1) - } - } - if (args.length < 1) { - bailOnFailure(runAllTestCases()) - return - } - let result = true - const map = makeTestCasesMap() - for (let i = 0; i < args.length; i++) { - const arg = args[i] - const testCase = map[arg] - if (testCase === undefined) { - console.log(`unknown test case: ${arg}`) - process.exit(1) - } - result = result && runTestCase(testCase) - } - bailOnFailure(result) -} - -// commandList implements the list command. -function commandList() { - for (let i = 0; i < testCases.length; i++) { - const testCase = testCases[i] - console.log(`${testCase.name}: ${testCase.description}`) - } -} - -// main is the main function. -function main() { - const usageAndExit = (exitcode) => { - console.log("usage: node ./QA/index.mjs list") - console.log("usage: node ./QA/index.mjs run [test_case_name...]") - process.exit(exitcode) - } - if (process.argv.length < 3) { - usageAndExit(0) - } - checkForDuplicateTestCases() - const command = process.argv[2] - switch (command) { - case "list": - commandList() - break - case "run": - commandRun(process.argv.slice(3)) - break - default: - console.log(`unknown command: ${command}`) - usageAndExit(1) - } -} - -main() diff --git a/QA/lib/analysis.mjs b/QA/lib/analysis.mjs deleted file mode 100644 index 413faf7..0000000 --- a/QA/lib/analysis.mjs +++ /dev/null @@ -1,196 +0,0 @@ -// This file contains code for analysing results. - -import { type } from "os" - -// checkAnnotations checks whether we have annotations. -function checkAnnotations(report) { - let result = true - - const isObject = typeof (report.annotations) === "object" - console.log(`checking whether annotations is an object... ${isObject}`) - result = result && isObject - - const hasArchitecture = typeof (report.annotations.architecture) === "string" - console.log(`checking whether annotations contains architecture... ${hasArchitecture}`) - result = result && hasArchitecture - - const hasEngineName = typeof (report.annotations.engine_name) === "string" - console.log(`checking whether annotations contains engine_name... ${hasEngineName}`) - result = result && hasEngineName - - const hasEngineVersion = typeof (report.annotations.engine_version) === "string" - console.log(`checking whether annotations contains engine_version... ${hasEngineVersion}`) - result = result && hasEngineVersion - - const hasPlatform = typeof (report.annotations.platform) === "string" - console.log(`checking whether annotations contains platform... ${hasPlatform}`) - result = result && hasPlatform - - return result -} - -// checkDataFormatVersion checks whether we have data_format_version. -function checkDataFormatVersion(report) { - const result = report.data_format_version === "0.2.0" - console.log(`checking whether we have the right data format version... ${result}`) - return result -} - -// checkExtensions ensures that extensions exists and has the right type. -function checkExtensions(report) { - const result = typeof (report.extensions) === "object" - console.log(`checking whether report.extensions is an object... ${result}`) - // Quirk: some experiments (e.g. web_connectivity) don't include - // an extensions object, and we don't want to fail here. - return true -} - -// checkInput ensures that the input is correct. -function checkInput(testCase, report) { - const result = testCase.input === report.input - console.log(`checking whether input is correct... ${result}`) - return result -} - -// checkExperimentName ensures that the experimentName is correctly -// set in the output report file. -function checkExperimentName(name, report) { - const result = report.test_name === name - console.log(`checking whether the experiment name is correct... ${result}`) - return result -} - -// checkStartTime ensures that a given start time is in the correct format. -function checkStartTime(report, key) { - const value = report[key] - let result = typeof (value) === "string" - console.log(`checking whether ${key} is a string... ${result}`) - result = result && value.match(/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/) !== null - console.log(`checking whether ${key} matches the date regexp... ${result}`) - return result -} - -// checkASN ensures that an ASN field is correct. -function checkASN(report, key) { - const value = report[key] - let result = typeof (value) === "string" - console.log(`checking whether ${key} is a string... ${result}`) - result = result && value.match(/^AS[0-9]+$/) !== null - console.log(`checking whether ${key} matches the ASN regexp... ${result}`) - return result -} - -// checkProbeCC ensures that the probe_cc field is correct. -function checkProbeCC(report) { - const value = report["probe_cc"] - let result = typeof (value) === "string" - console.log(`checking whether probe_cc is a string... ${result}`) - result = result && value.match(/^[A-Z]{2}$/) !== null - console.log(`checking whether probe_cc matches the CC regexp... ${result}`) - return result - -} - -// checkProbeIP ensures that the probe_ip field is correct. -function checkProbeIP(report) { - const result = report.probe_ip === "127.0.0.1" - console.log(`checking whether probe_ip is correct... ${result}`) - return result -} - -// checkReportID ensures that the report_id field is correct. -function checkReportID(report) { - const result = report.report_id === "" // note: we don't submit - console.log(`checking whether report_id is correct... ${result}`) - return result -} - -// checkString ensures that a field is a string. -function checkString(report, key) { - const value = report[key] - const result = typeof(value) === "string" - console.log(`checking whether ${key} is a string... ${result}`) - return result -} - -// checkNetworkName ensures that an xxx_network_name field is correct. -function checkNetworkName(report, key) { - return checkString(report, key) -} - -// checkResolverIP ensures that the resolver_ip field is correct. -function checkResolverIP(report) { - return checkString(report, "resolver_ip") -} - -// checkSoftwareName ensures that the software_name field is correct. -function checkSoftwareName(report) { - return checkString(report, "software_name") -} - -// checkSoftwareVersion ensures that the software_version field is correct. -function checkSoftwareVersion(report) { - return checkString(report, "software_version") -} - -// checkTestRuntime ensures that the test_runtime field is correct. -function checkTestRuntime(report) { - const result = typeof(report.test_runtime) === "number" - console.log(`checking whether test_runtime is a number... ${result}`) - return result -} - -// checkTestVersion ensures that the test_version field is correct. -function checkTestVersion(report) { - return checkString(report, "test_version") -} - -// checkTestKeys ensures that test_keys is an object. -function checkTestKeys(report) { - const result = typeof(report.test_keys) === "object" - console.log(`checking whether test_keys is an object... ${result}`) - return result -} - -// checkMeasurement is a function that invokes a standard set of checks -// to validate the top-level test keys of a result. -// -// This function helps to implement per-experiment checkers. -// -// Arguments: -// -// - testCase is the current test case -// -// - name is the name of the current experiment -// -// - report is the JSON measurement -// -// - extraChecks is the optional callback to perform extra checks -// that takes in input the report's test keys. -export function checkMeasurement(testCase, name, report, extraChecks) { - let result = true - result = result && checkAnnotations(report) - result = result && checkDataFormatVersion(report) - result = result && checkExtensions(report) - result = result && checkInput(testCase, report) - result = result && checkStartTime(report, "measurement_start_time") - result = result && checkASN(report, "probe_asn") - result = result && checkProbeCC(report) - result = result && checkProbeIP(report) - result = result && checkNetworkName(report, "probe_network_name") - result = result && checkReportID(report) - result = result && checkASN(report, "resolver_asn") - result = result && checkResolverIP(report) - result = result && checkNetworkName(report, "resolver_network_name") - result = result && checkSoftwareName(report) - result = result && checkSoftwareVersion(report) - result = result && checkExperimentName(name, report) - result = result && checkTestRuntime(report) - result = result && checkStartTime(report, "test_start_time") - result = result && checkTestVersion(report) - result = result && checkTestKeys(report) - if (typeof extraChecks === "function") { - result = result && extraChecks(report.test_keys) - } - return result -} diff --git a/QA/lib/blocking.mjs b/QA/lib/blocking.mjs deleted file mode 100644 index cc08f62..0000000 --- a/QA/lib/blocking.mjs +++ /dev/null @@ -1,17 +0,0 @@ -// This file contains helpers for describing blocking rules. - -// hijackPopularDNSServers returns an object containing the rules -// for hijacking popular DNS servers with `miniooni --censor`. -export function hijackPopularDNSServers() { - return { - // cloudflare - "1.1.1.1:53/udp": "hijack-dns", - "1.0.0.1:53/udp": "hijack-dns", - // google - "8.8.8.8:53/udp": "hijack-dns", - "8.8.4.4:53/udp": "hijack-dns", - // quad9 - "9.9.9.9:53/udp": "hijack-dns", - "9.9.9.10:53/udp": "hijack-dns", - } -} diff --git a/QA/lib/runner.mjs b/QA/lib/runner.mjs deleted file mode 100644 index 9371465..0000000 --- a/QA/lib/runner.mjs +++ /dev/null @@ -1,92 +0,0 @@ -// This file contains code for running test cases. - -import child_process from "child_process" -import crypto from "crypto" -import fs from "fs" -import path from "path" - -// tempFile returns the name of a temporary file. This function is -// not as secure as using mktemp but it does not matter in this -// context. We just need to create file names in the local directory -// with enough entropy that every run has a different name. -// -// See https://stackoverflow.com/questions/7055061 for an insightful -// discussion about creating temporary files in node. -function tempFile(suffix) { - return path.join(`tmp-${crypto.randomBytes(16).toString('hex')}.${suffix}`) -} - -// exec executes a command. This function throws on failure. The stdout -// and stderr should be console.log-ed once the command returns. -function exec(command) { - console.log(`+ ${command}`) - child_process.execSync(command) -} - -// writeCensorJsonFile writes a censor.json file using a file name -// containing random characters and returns the file name. -function writeCensorJsonFile(testCase) { - const fileName = tempFile("json") - fs.writeFileSync(fileName, JSON.stringify(testCase.blocking)) - return fileName -} - -// readReportFile reads and parses the report file, thus returning -// the JSON object contained inside the report file. -function readReportFile(reportFile) { - const data = fs.readFileSync(reportFile, { "encoding": "utf-8" }) - return JSON.parse(data) -} - -// runExperiment runs the given test case with the given experiment. -function runExperiment(testCase, experiment, checker) { - console.log(`## running: ${testCase.name}.${experiment}`) - console.log("") - const censorJson = writeCensorJsonFile(testCase) - const reportJson = tempFile("json") - // Note: using -n because we don't want to submit QA checks. - exec(`./miniooni -n --censor ${censorJson} -o ${reportJson} -i ${testCase.input} ${experiment}`) - console.log("") - // TODO(bassosimone): support multiple entries per file - const report = readReportFile(reportJson) - const analysisResult = checker(testCase, experiment, report) - console.log("") - console.log("") - if (analysisResult !== true && analysisResult !== false) { - console.log("the analysis function returned neither true nor false") - process.exit(1) - } - return analysisResult -} - -// recompileMiniooni recompiles miniooni if needed. -function recompileMiniooni() { - exec("go build -v ./internal/cmd/miniooni") -} - -// runTestCase runs the given test case. -// -// A test case is an object with the following fields: -// -// - name (string): the name of the test case; -// -// - description (string): a description of the test case; -// -// - input (string): the input to pass to the experiment; -// -// - blocking (object): a blocking specification (i.e., the -// serialization of a filtering.TProxyConfig struct); -// -// - experiments (object): the keys are names of nettests -// to run and the values are functions taking as their -// unique argument the experiment's test_keys. -export function runTestCase(testCase) { - recompileMiniooni() - console.log("") - console.log(`# running: ${testCase.name}`) - let result = true - for (const [name, checker] of Object.entries(testCase.experiments)) { - result = result && runExperiment(testCase, name, checker) - } - return result -} diff --git a/QA/lib/web.mjs b/QA/lib/web.mjs deleted file mode 100644 index 55ceb45..0000000 --- a/QA/lib/web.mjs +++ /dev/null @@ -1,641 +0,0 @@ -// This file contains test cases for web nettests. - -import { checkMeasurement } from "./analysis.mjs" - -// webConnectivityCheckTopLevel checks the top-level keys -// of the web connectivity experiment agains a template -// object. Returns true if they match, false on mismatch. -function webConnectivityCheckTopLevel(tk, template) { - let result = true - for (const [key, value] of Object.entries(template)) { - const check = tk[key] === value - console.log(`checking whether ${key}'s value is ${value}... ${check}`) - result = result && check - } - return result -} - -// Here we export all the test cases. Please, see the documentation -// of runner.runTestCase for a description of what is a test case. -export const testCases = [ - - // - // DNS checks - // - // We start with checks where _only_ the system resolver fails. - // - - { - name: "web_dns_system_nxdomain", - description: "the system resolver returns NXDOMAIN", - input: "https://nexa.polito.it/", - blocking: { - Domains: { - "nexa.polito.it": "nxdomain" - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": "dns_nxdomain_error", - "dns_consistency": "inconsistent", - "control_failure": null, - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "dns", - }) - return result - }) - }, - }, - }, - - { - name: "web_dns_system_refused", - description: "the system resolver returns REFUSED", - input: "https://nexa.polito.it/", - blocking: { - Domains: { - "nexa.polito.it": "refused" - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": "dns_refused_error", - "dns_consistency": "inconsistent", - "control_failure": null, - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": null, - "blocking": null, // TODO(bassosimone): this is clearly a bug - }) - return result - }) - }, - }, - }, - - { - name: "web_dns_system_localhost", - description: "the system resolver returns localhost", - input: "https://nexa.polito.it/", - blocking: { - Domains: { - "nexa.polito.it": "localhost", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - // TODO(bassosimone): Web Connectivity does not correctly handle this case - // but, still, correctly sets blocking as "dns" - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "inconsistent", - "control_failure": null, - "http_experiment_failure": "connection_refused", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "dns", - }) - return result - }) - }, - }, - }, - - { - name: "web_dns_system_bogon_not_localhost", - description: "the system resolver returns a bogon that is not localhost", - input: "https://nexa.polito.it/", - blocking: { - DNSCache: { - "nexa.polito.it": ["10.0.0.1"], - }, - Domains: { - "nexa.polito.it": "cache", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - // TODO(bassosimone): Web Connectivity does not correctly handle this case - // but, still, correctly sets blocking as "dns" - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "inconsistent", - "control_failure": null, - "http_experiment_failure": "generic_timeout_error", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "dns", - }) - return result - }) - }, - }, - }, - - { - name: "web_dns_system_no_answer", - description: "the system resolver returns an empty answer", - input: "https://nexa.polito.it/", - blocking: { - Domains: { - "nexa.polito.it": "no-answer", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": "dns_no_answer", - "dns_consistency": "inconsistent", - "control_failure": null, - "http_experiment_failure": null, - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": null, - "blocking": null, // TODO(bassosimone): this is clearly a bug - }) - return result - }) - }, - }, - }, - - { - name: "web_dns_system_timeout", - description: "the system resolver times out", - input: "https://nexa.polito.it/", - blocking: { - Domains: { - "nexa.polito.it": "timeout", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": "generic_timeout_error", - "dns_consistency": "inconsistent", - "control_failure": null, - "http_experiment_failure": null, - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": null, - "blocking": null, // TODO(bassosimone): this is clearly a bug - }) - return result - }) - }, - }, - - }, - - // TODO(bassosimone): here we should insert more checks where not only the system - // resolver is blocked but also other resolvers are. - - // - // TCP connect - // - // This section contains TCP connect failures. - // - - { - name: "web_tcp_connect_timeout", - description: "timeout when connecting to the IP address", - input: "https://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:443/tcp": "tcp-drop-syn", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "generic_timeout_error", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "tcp_ip", - }) - return result - }) - }, - }, - }, - - { - name: "web_tcp_connect_refused", - description: "connection refused when connecting to the IP address", - input: "https://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:443/tcp": "tcp-reject-syn", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "connection_refused", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "tcp_ip", - }) - return result - }) - }, - }, - }, - - // - // TLS handshake - // - // This section contains TLS handshake failures. - // - - { - name: "web_tls_handshake_timeout", - description: "timeout when performing the TLS handshake", - input: "https://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:443/tcp": "drop-data", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "generic_timeout_error", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "http-failure", - }) - return result - }) - }, - }, - }, - - { - name: "web_tls_handshake_reset", - description: "reset when performing the TLS handshake", - input: "https://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:443/tcp": "hijack-tls", - }, - SNIs: { - "nexa.polito.it": "reset", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "connection_reset", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "http-failure", - }) - return result - }) - }, - }, - }, - - // - // QUIC - // - - { - name: "web_quic_handshake_timeout", - description: "timeout when performing the QUIC handshake", - input: "https://dns.google/", - blocking: { - Endpoints: { - "8.8.8.8:443/udp": "drop-data", - "8.8.4.4:443/udp": "drop-data", - "[2001:4860:4860::8888]:443/udp": "drop-data", - "[2001:4860:4860::8844]:443/udp": "drop-data", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - }, - }, - - // - // Cleartext HTTP - // - - { - name: "web_http_reset", - description: "reset when performing the HTTP round trip", - input: "http://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:80/tcp": "hijack-http", - }, - Hosts: { - "nexa.polito.it": "reset", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "connection_reset", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "http-failure", - }) - return result - }) - }, - }, - }, - - { - name: "web_http_timeout", - description: "timeout when performing the HTTP round trip", - input: "http://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:80/tcp": "hijack-http", - }, - Hosts: { - "nexa.polito.it": "timeout", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "generic_timeout_error", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "http-failure", - }) - return result - }) - }, - }, - }, - - { - name: "web_http_451", - description: "451 when performing the HTTP round trip", - input: "http://nexa.polito.it/", - blocking: { - Endpoints: { - "130.192.16.171:80/tcp": "hijack-http", - }, - Hosts: { - "nexa.polito.it": "451", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - // TODO(bassosimone): there is no easy way to check for the body - // proportion robustly because it's a float. - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": null, - "body_length_match": false, - "status_code_match": false, - "headers_match": false, - "title_match": false, - "accessible": false, - "blocking": "http-diff", - }) - return result - }) - }, - }, - }, - - // - // More complex scenarios - // - - // In this scenario the second IP address for the domain fails - // with reset. Web Connectivity sees that but overall says it's - // all good because the good IP happens to be the first. We'll - // see what changes if we swap the IPs in the next scenario. - { - name: "web_tcp_second_ip_connection_reset", - description: "the second IP returned by DNS fails with connection reset", - input: "https://dns.google/", - blocking: { - DNSCache: { - "dns.google": ["8.8.4.4", "8.8.8.8"], - }, - Domains: { - "dns.google": "cache", - }, - Endpoints: { - "8.8.8.8:443/tcp": "hijack-tls", - }, - SNIs: { - "dns.google": "reset", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": null, - "body_length_match": true, - "body_proportion": 1, - "status_code_match": true, - "headers_match": true, - "title_match": true, - "accessible": true, - "blocking": false, - }) - return result - }) - }, - }, - }, - - // This scenario is like the previous one except that we swap - // the IP addresses and now Web Connectivity says failure. - { - name: "web_tcp_first_ip_connection_reset", - description: "the first IP returned by DNS fails with connection reset", - input: "https://dns.google/", - blocking: { - DNSCache: { - "dns.google": ["8.8.4.4", "8.8.8.8"], - }, - Domains: { - "dns.google": "cache", - }, - Endpoints: { - "8.8.4.4:443/tcp": "hijack-tls", - }, - SNIs: { - "dns.google": "reset", - }, - }, - experiments: { - websteps: (testCase, name, report) => { - return checkMeasurement(testCase, name, report) - }, - web_connectivity: (testCase, name, report) => { - return checkMeasurement(testCase, name, report, (tk) => { - let result = true - result = result && webConnectivityCheckTopLevel(tk, { - "dns_experiment_failure": null, - "dns_consistency": "consistent", - "control_failure": null, - "http_experiment_failure": "connection_reset", - "body_length_match": null, - "body_proportion": 0, - "status_code_match": null, - "headers_match": null, - "title_match": null, - "accessible": false, - "blocking": "http-failure", - }) - return result - }) - }, - }, - }, -] diff --git a/go.mod b/go.mod index a3278cf..b73aad3 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/hexops/gotextdiff v1.0.3 github.com/iancoleman/strcase v0.2.0 github.com/lucas-clemente/quic-go v0.27.0 - github.com/marten-seemann/qtls-go1-18 v0.1.1 github.com/mattn/go-colorable v0.1.12 github.com/miekg/dns v1.1.49 github.com/mitchellh/go-wordwrap v1.0.1 @@ -73,6 +72,7 @@ require ( github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect + github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.13 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect diff --git a/internal/archival/dialer.go b/internal/archival/dialer.go deleted file mode 100644 index 4258ae4..0000000 --- a/internal/archival/dialer.go +++ /dev/null @@ -1,86 +0,0 @@ -package archival - -// -// Saves dial and net.Conn events -// - -import ( - "context" - "net" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// NetworkEvent contains a network event. This kind of events -// are generated by Dialer, QUICDialer, Conn, QUICConn. -type NetworkEvent struct { - Count int - Failure error - Finished time.Time - Network string - Operation string - RemoteAddr string - Started time.Time -} - -// DialContext dials with the given dialer with the given arguments -// and stores the dial result inside of this saver. -func (s *Saver) DialContext(ctx context.Context, - dialer model.Dialer, network, address string) (net.Conn, error) { - started := time.Now() - conn, err := dialer.DialContext(ctx, network, address) - s.appendNetworkEvent(&NetworkEvent{ - Count: 0, - Failure: err, - Finished: time.Now(), - Network: network, - Operation: netxlite.ConnectOperation, - RemoteAddr: address, - Started: started, - }) - return conn, err -} - -// Read reads from the given conn and stores the results in the saver. -func (s *Saver) Read(conn net.Conn, buf []byte) (int, error) { - network := conn.RemoteAddr().Network() - remoteAddr := conn.RemoteAddr().String() - started := time.Now() - count, err := conn.Read(buf) - s.appendNetworkEvent(&NetworkEvent{ - Count: count, - Failure: err, - Finished: time.Now(), - Network: network, - Operation: netxlite.ReadOperation, - RemoteAddr: remoteAddr, - Started: started, - }) - return count, err -} - -// Write writes to the given conn and stores the results into the saver. -func (s *Saver) Write(conn net.Conn, buf []byte) (int, error) { - network := conn.RemoteAddr().Network() - remoteAddr := conn.RemoteAddr().String() - started := time.Now() - count, err := conn.Write(buf) - s.appendNetworkEvent(&NetworkEvent{ - Count: count, - Failure: err, - Finished: time.Now(), - Network: network, - Operation: netxlite.WriteOperation, - RemoteAddr: remoteAddr, - Started: started, - }) - return count, err -} - -func (s *Saver) appendNetworkEvent(ev *NetworkEvent) { - s.mu.Lock() - s.trace.Network = append(s.trace.Network, ev) - s.mu.Unlock() -} diff --git a/internal/archival/dialer_test.go b/internal/archival/dialer_test.go deleted file mode 100644 index 9ebc246..0000000 --- a/internal/archival/dialer_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package archival - -import ( - "context" - "errors" - "io" - "net" - "testing" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -func TestSaverDialContext(t *testing.T) { - // newConn creates a new connection with the desired properties. - newConn := func(address string) net.Conn { - return &mocks.Conn{ - MockRemoteAddr: func() net.Addr { - return &mocks.Addr{ - MockString: func() string { - return address - }, - } - }, - MockClose: func() error { - return nil - }, - } - } - - // newDialer creates a dialer for testing. - newDialer := func(conn net.Conn, err error) model.Dialer { - return &mocks.Dialer{ - MockDialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - time.Sleep(1 * time.Microsecond) - return conn, err - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - dialer := newDialer(newConn(mockedEndpoint), nil) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: nil, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.ConnectOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - ctx := context.Background() - conn, err := saver.DialContext(ctx, dialer, "tcp", mockedEndpoint) - if err != nil { - t.Fatal(err) - } - if conn == nil { - t.Fatal("expected non-nil conn") - } - conn.Close() - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - dialer := newDialer(nil, mockedError) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: mockedError, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.ConnectOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - ctx := context.Background() - conn, err := saver.DialContext(ctx, dialer, "tcp", mockedEndpoint) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected err", err) - } - if conn != nil { - t.Fatal("expected nil conn") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -func TestSaverRead(t *testing.T) { - // newConn is a helper function for creating a new connection. - newConn := func(endpoint string, numBytes int, err error) net.Conn { - return &mocks.Conn{ - MockRead: func(b []byte) (int, error) { - time.Sleep(time.Microsecond) - return numBytes, err - }, - MockRemoteAddr: func() net.Addr { - return &mocks.Addr{ - MockString: func() string { - return endpoint - }, - MockNetwork: func() string { - return "tcp" - }, - } - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - const mockedNumBytes = 128 - conn := newConn(mockedEndpoint, mockedNumBytes, nil) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: mockedNumBytes, - ExpectedErr: nil, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.ReadOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.Read(conn, buf) - if err != nil { - t.Fatal(err) - } - if count != mockedNumBytes { - t.Fatal("unexpected count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - conn := newConn(mockedEndpoint, 0, mockedError) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: mockedError, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.ReadOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.Read(conn, buf) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected err", err) - } - if count != 0 { - t.Fatal("unexpected count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -func TestSaverWrite(t *testing.T) { - // newConn is a helper function for creating a new connection. - newConn := func(endpoint string, numBytes int, err error) net.Conn { - return &mocks.Conn{ - MockWrite: func(b []byte) (int, error) { - time.Sleep(time.Microsecond) - return numBytes, err - }, - MockRemoteAddr: func() net.Addr { - return &mocks.Addr{ - MockString: func() string { - return endpoint - }, - MockNetwork: func() string { - return "tcp" - }, - } - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - const mockedNumBytes = 128 - conn := newConn(mockedEndpoint, mockedNumBytes, nil) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: mockedNumBytes, - ExpectedErr: nil, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.WriteOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.Write(conn, buf) - if err != nil { - t.Fatal(err) - } - if count != mockedNumBytes { - t.Fatal("unexpected count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - conn := newConn(mockedEndpoint, 0, mockedError) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: mockedError, - ExpectedNetwork: "tcp", - ExpectedOp: netxlite.WriteOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.Write(conn, buf) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected err", err) - } - if count != 0 { - t.Fatal("unexpected count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -// SingleNetworkEventValidator expects to find a single -// network event inside of the saver and ensures that such -// an event contains the required field values. -type SingleNetworkEventValidator struct { - ExpectedCount int - ExpectedErr error - ExpectedNetwork string - ExpectedOp string - ExpectedEpnt string - Saver *Saver -} - -func (v *SingleNetworkEventValidator) Validate() error { - trace := v.Saver.MoveOutTrace() - if len(trace.Network) != 1 { - return errors.New("expected to see a single .Network event") - } - entry := trace.Network[0] - if entry.Count != v.ExpectedCount { - return errors.New("expected to see a different .Count") - } - if !errors.Is(entry.Failure, v.ExpectedErr) { - return errors.New("unexpected .Failure") - } - if !entry.Finished.After(entry.Started) { - return errors.New(".Finished should be after .Started") - } - if entry.Network != v.ExpectedNetwork { - return errors.New("invalid value for .Network") - } - if entry.Operation != v.ExpectedOp { - return errors.New("invalid value for .Operation") - } - if entry.RemoteAddr != v.ExpectedEpnt { - return errors.New("unexpected value for .RemoteAddr") - } - return nil -} diff --git a/internal/archival/doc.go b/internal/archival/doc.go deleted file mode 100644 index 690cafd..0000000 --- a/internal/archival/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Package implements a Saver type that saves network, TCP, DNS, -// and TLS events. Given a Saver, you can export a Trace. Given a -// Trace you can obtain data in the OONI archival data format. -package archival diff --git a/internal/archival/http.go b/internal/archival/http.go deleted file mode 100644 index ff8890f..0000000 --- a/internal/archival/http.go +++ /dev/null @@ -1,107 +0,0 @@ -package archival - -// -// Saves HTTP events -// - -import ( - "bytes" - "io" - "net/http" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// HTTPRoundTripEvent contains an HTTP round trip. -type HTTPRoundTripEvent struct { - Failure error - Finished time.Time - Method string - RequestHeaders http.Header - ResponseBody []byte - ResponseBodyIsTruncated bool - ResponseBodyLength int64 - ResponseHeaders http.Header - Started time.Time - StatusCode int64 - Transport string - URL string -} - -// HTTPRoundTrip performs the round trip with the given transport and -// the given arguments and saves the results into the saver. -// -// The maxBodySnapshotSize argument controls the maximum size of the -// body snapshot that we collect along with the HTTP round trip. -func (s *Saver) HTTPRoundTrip( - txp model.HTTPTransport, maxBodySnapshotSize int64, - req *http.Request) (*http.Response, error) { - started := time.Now() - resp, err := txp.RoundTrip(req) - rt := &HTTPRoundTripEvent{ - Failure: nil, // set later - Finished: time.Time{}, // set later - Method: req.Method, - RequestHeaders: s.cloneRequestHeaders(req), - ResponseBody: nil, // set later - ResponseBodyIsTruncated: false, - ResponseBodyLength: 0, - ResponseHeaders: nil, // set later - Started: started, - StatusCode: 0, // set later - Transport: txp.Network(), - URL: req.URL.String(), - } - if err != nil { - rt.Finished = time.Now() - rt.Failure = err - s.appendHTTPRoundTripEvent(rt) - return nil, err - } - rt.StatusCode = int64(resp.StatusCode) - rt.ResponseHeaders = resp.Header.Clone() - r := io.LimitReader(resp.Body, maxBodySnapshotSize) - body, err := netxlite.ReadAllContext(req.Context(), r) - if err != nil { - rt.Finished = time.Now() - rt.Failure = err - s.appendHTTPRoundTripEvent(rt) - return nil, err - } - resp.Body = &archivalHTTPTransportBody{ // allow for reading again the whole body - Reader: io.MultiReader(bytes.NewReader(body), resp.Body), - Closer: resp.Body, - } - rt.ResponseBody = body - rt.ResponseBodyLength = int64(len(body)) - rt.ResponseBodyIsTruncated = int64(len(body)) >= maxBodySnapshotSize - rt.Finished = time.Now() - s.appendHTTPRoundTripEvent(rt) - return resp, nil -} - -// cloneRequestHeaders ensure we include the Host header among the saved -// headers, which is what OONI should do, even though the Go transport is -// such that this header is added later when we're sending the request. -func (s *Saver) cloneRequestHeaders(req *http.Request) http.Header { - header := req.Header.Clone() - if req.Host != "" { - header.Set("Host", req.Host) - } else { - header.Set("Host", req.URL.Host) - } - return header -} - -type archivalHTTPTransportBody struct { - io.Reader - io.Closer -} - -func (s *Saver) appendHTTPRoundTripEvent(ev *HTTPRoundTripEvent) { - s.mu.Lock() - s.trace.HTTPRoundTrip = append(s.trace.HTTPRoundTrip, ev) - s.mu.Unlock() -} diff --git a/internal/archival/http_test.go b/internal/archival/http_test.go deleted file mode 100644 index c08fd7b..0000000 --- a/internal/archival/http_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package archival - -import ( - "bytes" - "context" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/runtimex" -) - -func TestSaverHTTPRoundTrip(t *testing.T) { - // newHTTPTransport creates a new HTTP transport for testing. - newHTTPTransport := func(resp *http.Response, err error) model.HTTPTransport { - return &mocks.HTTPTransport{ - MockRoundTrip: func(req *http.Request) (*http.Response, error) { - return resp, err - }, - MockNetwork: func() string { - return "tcp" - }, - } - } - - // successFlowWithBody is a successful test case with possible body truncation. - successFlowWithBody := func(realBody []byte, maxBodySize int64) error { - // truncate the expected body if required - expectedBody := realBody - truncated := false - if int64(len(realBody)) > maxBodySize { - expectedBody = realBody[:maxBodySize] - truncated = true - } - // construct the saver and the validator - saver := NewSaver() - v := &SingleHTTPRoundTripValidator{ - ExpectFailure: nil, - ExpectMethod: "GET", - ExpectRequestHeaders: map[string][]string{ - "Host": {"127.0.0.1:8080"}, - "User-Agent": {"antani/1.0"}, - "X-Client-IP": {"130.192.91.211"}, - }, - ExpectResponseBody: expectedBody, - ExpectResponseBodyIsTruncated: truncated, - ExpectResponseBodyLength: int64(len(expectedBody)), - ExpectResponseHeaders: map[string][]string{ - "Server": {"antani/1.0"}, - "Content-Type": {"text/plain"}, - }, - ExpectStatusCode: 200, - ExpectTransport: "tcp", - ExpectURL: "http://127.0.0.1:8080/antani", - RealResponseBody: realBody, - Saver: saver, - } - // construct transport and perform the HTTP round trip - txp := newHTTPTransport(v.NewHTTPResponse(), nil) - resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest()) - if err != nil { - return err - } - if resp == nil { - return errors.New("expected non-nil resp") - } - // ensure that we can still read the _full_ response body - ctx := context.Background() - data, err := netxlite.ReadAllContext(ctx, resp.Body) - if err != nil { - return err - } - if diff := cmp.Diff(realBody, data); diff != "" { - return errors.New(diff) - } - // validate the content of the trace - return v.Validate() - } - - t.Run("on success without truncation", func(t *testing.T) { - realBody := []byte("0xdeadbeef") - const maxBodySize = 1 << 20 - err := successFlowWithBody(realBody, maxBodySize) - if err != nil { - t.Fatal(err) - } - }) - - t.Run("on success with truncation", func(t *testing.T) { - realBody := []byte("0xdeadbeef") - const maxBodySize = 4 - err := successFlowWithBody(realBody, maxBodySize) - if err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure during round trip", func(t *testing.T) { - expectedError := netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET) - const maxBodySize = 1 << 20 - saver := NewSaver() - v := &SingleHTTPRoundTripValidator{ - ExpectFailure: expectedError, - ExpectMethod: "GET", - ExpectRequestHeaders: map[string][]string{ - "Host": {"127.0.0.1:8080"}, - "User-Agent": {"antani/1.0"}, - "X-Client-IP": {"130.192.91.211"}, - }, - ExpectResponseBody: nil, - ExpectResponseBodyIsTruncated: false, - ExpectResponseBodyLength: 0, - ExpectResponseHeaders: nil, - ExpectStatusCode: 0, - ExpectTransport: "tcp", - ExpectURL: "http://127.0.0.1:8080/antani", - RealResponseBody: nil, - Saver: saver, - } - txp := newHTTPTransport(nil, expectedError) - resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest()) - if !errors.Is(err, expectedError) { - t.Fatal(err) - } - if resp != nil { - t.Fatal("expected nil resp") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure reading body", func(t *testing.T) { - expectedError := netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET) - const maxBodySize = 1 << 20 - saver := NewSaver() - v := &SingleHTTPRoundTripValidator{ - ExpectFailure: expectedError, - ExpectMethod: "GET", - ExpectRequestHeaders: map[string][]string{ - "Host": {"127.0.0.1:8080"}, - "User-Agent": {"antani/1.0"}, - "X-Client-IP": {"130.192.91.211"}, - }, - ExpectResponseBody: nil, - ExpectResponseBodyIsTruncated: false, - ExpectResponseBodyLength: 0, - ExpectResponseHeaders: map[string][]string{ - "Server": {"antani/1.0"}, - "Content-Type": {"text/plain"}, - }, - ExpectStatusCode: 200, - ExpectTransport: "tcp", - ExpectURL: "http://127.0.0.1:8080/antani", - RealResponseBody: nil, - Saver: saver, - } - resp := v.NewHTTPResponse() - // Hack the body so it returns a connection reset error - // after some useful piece of data. We do not see any - // body in the response or in the trace. We may possibly - // want to include all the body we could read into the - // trace in the future, but for now it seems fine to do - // exactly what the previous code was doing. - resp.Body = io.NopCloser(io.MultiReader( - bytes.NewReader([]byte("0xdeadbeef")), - &mocks.Reader{ - MockRead: func(b []byte) (int, error) { - return 0, expectedError - }, - }, - )) - txp := newHTTPTransport(resp, nil) - resp, err := saver.HTTPRoundTrip(txp, maxBodySize, v.NewHTTPRequest()) - if !errors.Is(err, expectedError) { - t.Fatal("unexpected err", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("cloneRequestHeaders", func(t *testing.T) { - // doWithRequest is an helper function that creates a suitable - // round tripper and returns trace.HTTPRoundTrip[0] for inspection - doWithRequest := func(req *http.Request) (*HTTPRoundTripEvent, error) { - expect := errors.New("mocked err") - txp := newHTTPTransport(nil, expect) - saver := NewSaver() - const maxBodySize = 1 << 20 // irrelevant - resp, err := saver.HTTPRoundTrip(txp, maxBodySize, req) - if !errors.Is(err, expect) { - return nil, fmt.Errorf("unexpected error: %w", err) - } - if resp != nil { - return nil, errors.New("expected nil resp") - } - trace := saver.MoveOutTrace() - if len(trace.HTTPRoundTrip) != 1 { - return nil, errors.New("expected exactly one HTTPRoundTrip") - } - return trace.HTTPRoundTrip[0], nil - } - - t.Run("with req.URL.Host", func(t *testing.T) { - req, err := http.NewRequest("GET", "https://x.org/", nil) - if err != nil { - t.Fatal(err) - } - ev, err := doWithRequest(req) - if err != nil { - t.Fatal(err) - } - if ev.RequestHeaders.Get("Host") != "x.org" { - t.Fatal("unexpected request host") - } - }) - - t.Run("with req.Host", func(t *testing.T) { - req, err := http.NewRequest("GET", "https://x.org/", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "google.com" - ev, err := doWithRequest(req) - if err != nil { - t.Fatal(err) - } - if ev.RequestHeaders.Get("Host") != "google.com" { - t.Fatal("unexpected request host") - } - }) - }) -} - -type SingleHTTPRoundTripValidator struct { - ExpectFailure error - ExpectMethod string - ExpectRequestHeaders http.Header - ExpectResponseBody []byte - ExpectResponseBodyIsTruncated bool - ExpectResponseBodyLength int64 - ExpectResponseHeaders http.Header - ExpectStatusCode int64 - ExpectTransport string - ExpectURL string - RealResponseBody []byte - Saver *Saver -} - -func (v *SingleHTTPRoundTripValidator) NewHTTPRequest() *http.Request { - parsedURL, err := url.Parse(v.ExpectURL) - runtimex.PanicOnError(err, "url.Parse should not fail here") - // The saving code clones the headers and adds the host header, which - // Go would instead add later. So, a realistic mock should not include - // such an header inside of the http.Request. - clonedHeaders := v.ExpectRequestHeaders.Clone() - clonedHeaders.Del("Host") - return &http.Request{ - Method: v.ExpectMethod, - URL: parsedURL, - Proto: "", - ProtoMajor: 0, - ProtoMinor: 0, - Header: clonedHeaders, - Body: nil, - GetBody: nil, - ContentLength: 0, - TransferEncoding: nil, - Close: false, - Host: "", - Form: nil, - PostForm: nil, - MultipartForm: nil, - Trailer: nil, - RemoteAddr: "", - RequestURI: "", - TLS: nil, - Cancel: nil, - Response: nil, - } -} - -func (v *SingleHTTPRoundTripValidator) NewHTTPResponse() *http.Response { - body := io.NopCloser(bytes.NewReader(v.RealResponseBody)) - return &http.Response{ - Status: http.StatusText(int(v.ExpectStatusCode)), - StatusCode: int(v.ExpectStatusCode), - Proto: "", - ProtoMajor: 0, - ProtoMinor: 0, - Header: v.ExpectResponseHeaders, - Body: body, - ContentLength: 0, - TransferEncoding: nil, - Close: false, - Uncompressed: false, - Trailer: nil, - Request: nil, - TLS: nil, - } -} - -func (v *SingleHTTPRoundTripValidator) Validate() error { - trace := v.Saver.MoveOutTrace() - if len(trace.HTTPRoundTrip) != 1 { - return errors.New("expected to see one event") - } - entry := trace.HTTPRoundTrip[0] - if !errors.Is(entry.Failure, v.ExpectFailure) { - return errors.New("unexpected .Failure") - } - if !entry.Finished.After(entry.Started) { - return errors.New(".Finished is not after .Started") - } - if entry.Method != v.ExpectMethod { - return errors.New("unexpected .Method") - } - if diff := cmp.Diff(v.ExpectRequestHeaders, entry.RequestHeaders); diff != "" { - return errors.New(diff) - } - if diff := cmp.Diff(v.ExpectResponseBody, entry.ResponseBody); diff != "" { - return errors.New(diff) - } - if entry.ResponseBodyIsTruncated != v.ExpectResponseBodyIsTruncated { - return errors.New("unexpected .ResponseBodyIsTruncated") - } - if entry.ResponseBodyLength != v.ExpectResponseBodyLength { - return errors.New("unexpected .ResponseBodyLength") - } - if diff := cmp.Diff(v.ExpectResponseHeaders, entry.ResponseHeaders); diff != "" { - return errors.New(diff) - } - if entry.StatusCode != v.ExpectStatusCode { - return errors.New("unexpected .StatusCode") - } - if entry.Transport != v.ExpectTransport { - return errors.New("unexpected .Transport") - } - if entry.URL != v.ExpectURL { - return errors.New("unexpected .URL") - } - return nil -} diff --git a/internal/archival/quic.go b/internal/archival/quic.go deleted file mode 100644 index 3a56976..0000000 --- a/internal/archival/quic.go +++ /dev/null @@ -1,91 +0,0 @@ -package archival - -// -// Saves QUIC events. -// - -import ( - "context" - "crypto/tls" - "net" - "time" - - "github.com/lucas-clemente/quic-go" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// WriteTo performs WriteTo with the given pconn and saves the -// operation's results inside the saver. -func (s *Saver) WriteTo(pconn model.UDPLikeConn, buf []byte, addr net.Addr) (int, error) { - started := time.Now() - count, err := pconn.WriteTo(buf, addr) - s.appendNetworkEvent(&NetworkEvent{ - Count: count, - Failure: err, - Finished: time.Now(), - Network: addr.Network(), - Operation: netxlite.WriteToOperation, - RemoteAddr: addr.String(), - Started: started, - }) - return count, err -} - -// ReadFrom performs ReadFrom with the given pconn and saves the -// operation's results inside the saver. -func (s *Saver) ReadFrom(pconn model.UDPLikeConn, buf []byte) (int, net.Addr, error) { - started := time.Now() - count, addr, err := pconn.ReadFrom(buf) - s.appendNetworkEvent(&NetworkEvent{ - Count: count, - Failure: err, - Finished: time.Now(), - Network: "udp", // must be always set even on failure - Operation: netxlite.ReadFromOperation, - RemoteAddr: s.safeAddrString(addr), - Started: started, - }) - return count, addr, err -} - -func (s *Saver) safeAddrString(addr net.Addr) (out string) { - if addr != nil { - out = addr.String() - } - return -} - -// QUICDialContext dials a QUIC session using the given dialer -// and saves the results inside of the saver. -func (s *Saver) QUICDialContext(ctx context.Context, dialer model.QUICDialer, - network, address string, tlsConfig *tls.Config, quicConfig *quic.Config) (quic.EarlyConnection, error) { - started := time.Now() - var state tls.ConnectionState - sess, err := dialer.DialContext(ctx, network, address, tlsConfig, quicConfig) - if err == nil { - <-sess.HandshakeComplete().Done() // robustness (the dialer already does that) - state = sess.ConnectionState().TLS.ConnectionState - } - s.appendQUICHandshake(&QUICTLSHandshakeEvent{ - ALPN: tlsConfig.NextProtos, - CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite), - Failure: err, - Finished: time.Now(), - NegotiatedProto: state.NegotiatedProtocol, - Network: "quic", - PeerCerts: s.tlsPeerCerts(err, &state), - RemoteAddr: address, - SNI: tlsConfig.ServerName, - SkipVerify: tlsConfig.InsecureSkipVerify, - Started: started, - TLSVersion: netxlite.TLSVersionString(state.Version), - }) - return sess, err -} - -func (s *Saver) appendQUICHandshake(ev *QUICTLSHandshakeEvent) { - s.mu.Lock() - s.trace.QUICHandshake = append(s.trace.QUICHandshake, ev) - s.mu.Unlock() -} diff --git a/internal/archival/quic_test.go b/internal/archival/quic_test.go deleted file mode 100644 index d5a086f..0000000 --- a/internal/archival/quic_test.go +++ /dev/null @@ -1,420 +0,0 @@ -package archival - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "io" - "net" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/lucas-clemente/quic-go" - "github.com/marten-seemann/qtls-go1-18" // it's annoying to depend on that - "github.com/ooni/probe-cli/v3/internal/fakefill" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -func TestSaverWriteTo(t *testing.T) { - // newAddr creates an new net.Addr for testing. - newAddr := func(endpoint string) net.Addr { - return &mocks.Addr{ - MockString: func() string { - return endpoint - }, - MockNetwork: func() string { - return "udp" - }, - } - } - - // newConn is a helper function for creating a new connection. - newConn := func(numBytes int, err error) model.UDPLikeConn { - return &mocks.UDPLikeConn{ - MockWriteTo: func(p []byte, addr net.Addr) (int, error) { - time.Sleep(time.Microsecond) - return numBytes, err - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - const mockedNumBytes = 128 - addr := newAddr(mockedEndpoint) - conn := newConn(mockedNumBytes, nil) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: mockedNumBytes, - ExpectedErr: nil, - ExpectedNetwork: "udp", - ExpectedOp: netxlite.WriteToOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.WriteTo(conn, buf, addr) - if err != nil { - t.Fatal(err) - } - if count != mockedNumBytes { - t.Fatal("invalid count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - addr := newAddr(mockedEndpoint) - conn := newConn(0, mockedError) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: mockedError, - ExpectedNetwork: "udp", - ExpectedOp: netxlite.WriteToOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, err := saver.WriteTo(conn, buf, addr) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected err", err) - } - if count != 0 { - t.Fatal("invalid count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -func TestSaverReadFrom(t *testing.T) { - // newAddr creates an new net.Addr for testing. - newAddr := func(endpoint string) net.Addr { - return &mocks.Addr{ - MockString: func() string { - return endpoint - }, - MockNetwork: func() string { - return "udp" - }, - } - } - - // newConn is a helper function for creating a new connection. - newConn := func(numBytes int, addr net.Addr, err error) model.UDPLikeConn { - return &mocks.UDPLikeConn{ - MockReadFrom: func(p []byte) (int, net.Addr, error) { - time.Sleep(time.Microsecond) - return numBytes, addr, err - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - const mockedNumBytes = 128 - expectedAddr := newAddr(mockedEndpoint) - conn := newConn(mockedNumBytes, expectedAddr, nil) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: mockedNumBytes, - ExpectedErr: nil, - ExpectedNetwork: "udp", - ExpectedOp: netxlite.ReadFromOperation, - ExpectedEpnt: mockedEndpoint, - Saver: saver, - } - buf := make([]byte, 1024) - count, addr, err := saver.ReadFrom(conn, buf) - if err != nil { - t.Fatal(err) - } - if expectedAddr.Network() != addr.Network() { - t.Fatal("invalid addr.Network") - } - if expectedAddr.String() != addr.String() { - t.Fatal("invalid addr.String") - } - if count != mockedNumBytes { - t.Fatal("invalid count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - conn := newConn(0, nil, mockedError) - saver := NewSaver() - v := &SingleNetworkEventValidator{ - ExpectedCount: 0, - ExpectedErr: mockedError, - ExpectedNetwork: "udp", - ExpectedOp: netxlite.ReadFromOperation, - ExpectedEpnt: "", - Saver: saver, - } - buf := make([]byte, 1024) - count, addr, err := saver.ReadFrom(conn, buf) - if !errors.Is(err, mockedError) { - t.Fatal(err) - } - if addr != nil { - t.Fatal("invalid addr") - } - if count != 0 { - t.Fatal("invalid count") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -func TestSaverQUICDialContext(t *testing.T) { - // newQUICDialer creates a new QUICDialer for testing. - newQUICDialer := func(qconn quic.EarlyConnection, err error) model.QUICDialer { - return &mocks.QUICDialer{ - MockDialContext: func( - ctx context.Context, network, address string, tlsConfig *tls.Config, - quicConfig *quic.Config) (quic.EarlyConnection, error) { - time.Sleep(time.Microsecond) - return qconn, err - }, - } - } - - // newQUICConnection creates a new quic.EarlyConnection for testing. - newQUICConnection := func(handshakeComplete context.Context, state tls.ConnectionState) quic.EarlyConnection { - return &mocks.QUICEarlyConnection{ - MockHandshakeComplete: func() context.Context { - return handshakeComplete - }, - MockConnectionState: func() quic.ConnectionState { - return quic.ConnectionState{ - TLS: qtls.ConnectionStateWith0RTT{ - ConnectionState: state, - }, - } - }, - MockCloseWithError: func(code quic.ApplicationErrorCode, reason string) error { - return nil - }, - } - } - - t.Run("on success", func(t *testing.T) { - handshakeCtx := context.Background() - handshakeCtx, handshakeCancel := context.WithCancel(handshakeCtx) - handshakeCancel() // simulate a completed handshake - const expectedNetwork = "udp" - const mockedEndpoint = "8.8.4.4:443" - saver := NewSaver() - var peerCerts [][]byte - ff := &fakefill.Filler{} - ff.Fill(&peerCerts) - if len(peerCerts) < 1 { - t.Fatal("did not fill peerCerts") - } - v := &SingleQUICTLSHandshakeValidator{ - ExpectedALPN: []string{"h3"}, - ExpectedSNI: "dns.google", - ExpectedSkipVerify: true, - // - ExpectedCipherSuite: tls.TLS_AES_128_GCM_SHA256, - ExpectedNegotiatedProtocol: "h3", - ExpectedPeerCerts: peerCerts, - ExpectedVersion: tls.VersionTLS13, - // - ExpectedNetwork: "quic", - ExpectedRemoteAddr: mockedEndpoint, - // - QUICConfig: &quic.Config{}, - // - ExpectedFailure: nil, - Saver: saver, - } - qconn := newQUICConnection(handshakeCtx, v.NewTLSConnectionState()) - dialer := newQUICDialer(qconn, nil) - ctx := context.Background() - qconn, err := saver.QUICDialContext(ctx, dialer, expectedNetwork, - mockedEndpoint, v.NewTLSConfig(), v.QUICConfig) - if err != nil { - t.Fatal(err) - } - if qconn == nil { - t.Fatal("expected nil qconn") - } - qconn.CloseWithError(0, "") - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on other error", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - const expectedNetwork = "udp" - const mockedEndpoint = "8.8.4.4:443" - saver := NewSaver() - v := &SingleQUICTLSHandshakeValidator{ - ExpectedALPN: []string{"h3"}, - ExpectedSNI: "dns.google", - ExpectedSkipVerify: true, - // - ExpectedCipherSuite: 0, - ExpectedNegotiatedProtocol: "", - ExpectedPeerCerts: nil, - ExpectedVersion: 0, - // - ExpectedNetwork: "quic", - ExpectedRemoteAddr: mockedEndpoint, - // - QUICConfig: &quic.Config{}, - // - ExpectedFailure: mockedError, - Saver: saver, - } - dialer := newQUICDialer(nil, mockedError) - ctx := context.Background() - qconn, err := saver.QUICDialContext(ctx, dialer, expectedNetwork, - mockedEndpoint, v.NewTLSConfig(), v.QUICConfig) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected error") - } - if qconn != nil { - t.Fatal("expected nil connection") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - // TODO(bassosimone): here we're not testing the case in which - // the certificate is invalid for the required SNI. - // - // We need first to figure out whether this is what happens - // when we validate for QUIC in such cases. If that's the case - // indeed, then we can write the tests. - - t.Run("on x509.HostnameError", func(t *testing.T) { - t.Skip("test not implemented") - }) - - t.Run("on x509.UnknownAuthorityError", func(t *testing.T) { - t.Skip("test not implemented") - }) - - t.Run("on x509.CertificateInvalidError", func(t *testing.T) { - t.Skip("test not implemented") - }) -} - -type SingleQUICTLSHandshakeValidator struct { - // related to the tls.Config - ExpectedALPN []string - ExpectedSNI string - ExpectedSkipVerify bool - - // related to the tls.ConnectionState - ExpectedCipherSuite uint16 - ExpectedNegotiatedProtocol string - ExpectedPeerCerts [][]byte - ExpectedVersion uint16 - - // related to the mocked conn (TLS) / dial params (QUIC) - ExpectedNetwork string - ExpectedRemoteAddr string - - // tells us whether we're using QUIC - QUICConfig *quic.Config - - // other fields - ExpectedFailure error - Saver *Saver -} - -func (v *SingleQUICTLSHandshakeValidator) NewTLSConfig() *tls.Config { - return &tls.Config{ - NextProtos: v.ExpectedALPN, - ServerName: v.ExpectedSNI, - InsecureSkipVerify: v.ExpectedSkipVerify, - } -} - -func (v *SingleQUICTLSHandshakeValidator) NewTLSConnectionState() tls.ConnectionState { - var state tls.ConnectionState - if v.ExpectedCipherSuite != 0 { - state.CipherSuite = v.ExpectedCipherSuite - } - if v.ExpectedNegotiatedProtocol != "" { - state.NegotiatedProtocol = v.ExpectedNegotiatedProtocol - } - for _, cert := range v.ExpectedPeerCerts { - state.PeerCertificates = append(state.PeerCertificates, &x509.Certificate{ - Raw: cert, - }) - } - if v.ExpectedVersion != 0 { - state.Version = v.ExpectedVersion - } - return state -} - -func (v *SingleQUICTLSHandshakeValidator) Validate() error { - trace := v.Saver.MoveOutTrace() - var entries []*QUICTLSHandshakeEvent - if v.QUICConfig != nil { - entries = trace.QUICHandshake - } else { - entries = trace.TLSHandshake - } - if len(entries) != 1 { - return errors.New("expected to see a single entry") - } - entry := entries[0] - if diff := cmp.Diff(entry.ALPN, v.ExpectedALPN); diff != "" { - return errors.New(diff) - } - if entry.CipherSuite != netxlite.TLSCipherSuiteString(v.ExpectedCipherSuite) { - return errors.New("unexpected .CipherSuite") - } - if !errors.Is(entry.Failure, v.ExpectedFailure) { - return errors.New("unexpected .Failure") - } - if !entry.Finished.After(entry.Started) { - return errors.New(".Finished is not after .Started") - } - if entry.NegotiatedProto != v.ExpectedNegotiatedProtocol { - return errors.New("unexpected .NegotiatedProto") - } - if entry.Network != v.ExpectedNetwork { - return errors.New("unexpected .Network") - } - if diff := cmp.Diff(entry.PeerCerts, v.ExpectedPeerCerts); diff != "" { - return errors.New("unexpected .PeerCerts") - } - if entry.RemoteAddr != v.ExpectedRemoteAddr { - return errors.New("unexpected .RemoteAddr") - } - if entry.SNI != v.ExpectedSNI { - return errors.New("unexpected .ServerName") - } - if entry.SkipVerify != v.ExpectedSkipVerify { - return errors.New("unexpected .SkipVerify") - } - if entry.TLSVersion != netxlite.TLSVersionString(v.ExpectedVersion) { - return errors.New("unexpected .Version") - } - return nil -} diff --git a/internal/archival/resolver.go b/internal/archival/resolver.go deleted file mode 100644 index 7ad34e0..0000000 --- a/internal/archival/resolver.go +++ /dev/null @@ -1,123 +0,0 @@ -package archival - -// -// Saves DNS lookup events -// - -import ( - "context" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" -) - -// DNSLookupEvent contains the results of a DNS lookup. -type DNSLookupEvent struct { - ALPNs []string - Addresses []string - Domain string - Failure error - Finished time.Time - LookupType string - ResolverAddress string - ResolverNetwork string - Started time.Time -} - -// LookupHost performs a host lookup with the given resolver -// and saves the results into the saver. -func (s *Saver) LookupHost(ctx context.Context, reso model.Resolver, domain string) ([]string, error) { - started := time.Now() - addrs, err := reso.LookupHost(ctx, domain) - s.appendLookupHostEvent(&DNSLookupEvent{ - ALPNs: nil, - Addresses: addrs, - Domain: domain, - Failure: err, - Finished: time.Now(), - LookupType: "getaddrinfo", - ResolverAddress: reso.Address(), - ResolverNetwork: reso.Network(), - Started: started, - }) - return addrs, err -} - -func (s *Saver) appendLookupHostEvent(ev *DNSLookupEvent) { - s.mu.Lock() - s.trace.DNSLookupHost = append(s.trace.DNSLookupHost, ev) - s.mu.Unlock() -} - -// LookupHTTPS performs an HTTPSSvc-record lookup using the given -// resolver and saves the results into the saver. -func (s *Saver) LookupHTTPS(ctx context.Context, reso model.Resolver, domain string) (*model.HTTPSSvc, error) { - started := time.Now() - https, err := reso.LookupHTTPS(ctx, domain) - s.appendLookupHTTPSEvent(&DNSLookupEvent{ - ALPNs: s.safeALPNs(https), - Addresses: s.safeAddresses(https), - Domain: domain, - Failure: err, - Finished: time.Now(), - LookupType: "https", - ResolverAddress: reso.Address(), - ResolverNetwork: reso.Network(), - Started: started, - }) - return https, err -} - -func (s *Saver) appendLookupHTTPSEvent(ev *DNSLookupEvent) { - s.mu.Lock() - s.trace.DNSLookupHTTPS = append(s.trace.DNSLookupHTTPS, ev) - s.mu.Unlock() -} - -func (s *Saver) safeALPNs(https *model.HTTPSSvc) (out []string) { - if https != nil { - out = https.ALPN - } - return -} - -func (s *Saver) safeAddresses(https *model.HTTPSSvc) (out []string) { - if https != nil { - out = append(out, https.IPv4...) - out = append(out, https.IPv6...) - } - return -} - -// DNSRoundTripEvent contains the result of a DNS round trip. -type DNSRoundTripEvent struct { - Address string - Failure error - Finished time.Time - Network string - Query []byte - Reply []byte - Started time.Time -} - -// DNSRoundTrip implements ArchivalSaver.DNSRoundTrip. -func (s *Saver) DNSRoundTrip(ctx context.Context, txp model.DNSTransport, query []byte) ([]byte, error) { - started := time.Now() - reply, err := txp.RoundTrip(ctx, query) - s.appendDNSRoundTripEvent(&DNSRoundTripEvent{ - Address: txp.Address(), - Failure: err, - Finished: time.Now(), - Network: txp.Network(), - Query: query, - Reply: reply, - Started: started, - }) - return reply, err -} - -func (s *Saver) appendDNSRoundTripEvent(ev *DNSRoundTripEvent) { - s.mu.Lock() - s.trace.DNSRoundTrip = append(s.trace.DNSRoundTrip, ev) - s.mu.Unlock() -} diff --git a/internal/archival/resolver_test.go b/internal/archival/resolver_test.go deleted file mode 100644 index 96d4807..0000000 --- a/internal/archival/resolver_test.go +++ /dev/null @@ -1,347 +0,0 @@ -package archival - -import ( - "context" - "errors" - "io" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/fakefill" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -func TestSaverLookupHost(t *testing.T) { - // newResolver helps to create a new resolver. - newResolver := func(addrs []string, err error) model.Resolver { - return &mocks.Resolver{ - MockLookupHost: func(ctx context.Context, domain string) ([]string, error) { - return addrs, err - }, - MockAddress: func() string { - return "8.8.8.8:53" - }, - MockNetwork: func() string { - return "udp" - }, - } - } - - t.Run("on success", func(t *testing.T) { - const domain = "dns.google" - expectAddrs := []string{"8.8.8.8", "8.8.4.4"} - saver := NewSaver() - v := &SingleDNSLookupValidator{ - ExpectALPNs: nil, - ExpectAddrs: expectAddrs, - ExpectDomain: domain, - ExpectLookupType: "getaddrinfo", - ExpectFailure: nil, - ExpectResolverAddress: "8.8.8.8:53", - ExpectResolverNetwork: "udp", - Saver: saver, - } - reso := newResolver(expectAddrs, nil) - ctx := context.Background() - addrs, err := saver.LookupHost(ctx, reso, domain) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(expectAddrs, addrs); diff != "" { - t.Fatal(diff) - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - const domain = "dns.google" - saver := NewSaver() - v := &SingleDNSLookupValidator{ - ExpectALPNs: nil, - ExpectAddrs: nil, - ExpectDomain: domain, - ExpectLookupType: "getaddrinfo", - ExpectFailure: mockedError, - ExpectResolverAddress: "8.8.8.8:53", - ExpectResolverNetwork: "udp", - Saver: saver, - } - reso := newResolver(nil, mockedError) - ctx := context.Background() - addrs, err := saver.LookupHost(ctx, reso, domain) - if !errors.Is(err, mockedError) { - t.Fatal("invalid err", err) - } - if len(addrs) != 0 { - t.Fatal("invalid addrs") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -func TestSaverLookupHTTPS(t *testing.T) { - // newResolver helps to create a new resolver. - newResolver := func(alpns, ipv4, ipv6 []string, err error) model.Resolver { - return &mocks.Resolver{ - MockLookupHTTPS: func(ctx context.Context, domain string) (*model.HTTPSSvc, error) { - if alpns == nil && ipv4 == nil && ipv6 == nil { - return nil, err - } - return &model.HTTPSSvc{ - ALPN: alpns, - IPv4: ipv4, - IPv6: ipv6, - }, err - }, - MockAddress: func() string { - return "8.8.8.8:53" - }, - MockNetwork: func() string { - return "udp" - }, - } - } - - t.Run("on success", func(t *testing.T) { - const domain = "dns.google" - expectALPN := []string{"h3", "h2", "http/1.1"} - expectA := []string{"8.8.8.8", "8.8.4.4"} - expectAAAA := []string{"2001:4860:4860::8844"} - expectAddrs := append(expectA, expectAAAA...) - saver := NewSaver() - v := &SingleDNSLookupValidator{ - ExpectALPNs: expectALPN, - ExpectAddrs: expectAddrs, - ExpectDomain: domain, - ExpectLookupType: "https", - ExpectFailure: nil, - ExpectResolverAddress: "8.8.8.8:53", - ExpectResolverNetwork: "udp", - Saver: saver, - } - reso := newResolver(expectALPN, expectA, expectAAAA, nil) - ctx := context.Background() - https, err := saver.LookupHTTPS(ctx, reso, domain) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(expectALPN, https.ALPN); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(expectA, https.IPv4); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(expectAAAA, https.IPv6); diff != "" { - t.Fatal(diff) - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - const domain = "dns.google" - saver := NewSaver() - v := &SingleDNSLookupValidator{ - ExpectALPNs: nil, - ExpectAddrs: nil, - ExpectDomain: domain, - ExpectLookupType: "https", - ExpectFailure: mockedError, - ExpectResolverAddress: "8.8.8.8:53", - ExpectResolverNetwork: "udp", - Saver: saver, - } - reso := newResolver(nil, nil, nil, mockedError) - ctx := context.Background() - https, err := saver.LookupHTTPS(ctx, reso, domain) - if !errors.Is(err, mockedError) { - t.Fatal("unexpected err", err) - } - if https != nil { - t.Fatal("expected nil https") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -type SingleDNSLookupValidator struct { - ExpectALPNs []string - ExpectAddrs []string - ExpectDomain string - ExpectLookupType string - ExpectFailure error - ExpectResolverAddress string - ExpectResolverNetwork string - Saver *Saver -} - -func (v *SingleDNSLookupValidator) Validate() error { - trace := v.Saver.MoveOutTrace() - var entries []*DNSLookupEvent - switch v.ExpectLookupType { - case "getaddrinfo": - entries = trace.DNSLookupHost - case "https": - entries = trace.DNSLookupHTTPS - default: - return errors.New("invalid v.ExpectLookupType") - } - if len(entries) != 1 { - return errors.New("expected a single entry") - } - entry := entries[0] - if diff := cmp.Diff(v.ExpectALPNs, entry.ALPNs); diff != "" { - return errors.New(diff) - } - if diff := cmp.Diff(v.ExpectAddrs, entry.Addresses); diff != "" { - return errors.New(diff) - } - if v.ExpectDomain != entry.Domain { - return errors.New("invalid .Domain value") - } - if !errors.Is(entry.Failure, v.ExpectFailure) { - return errors.New("invalid .Failure value") - } - if !entry.Finished.After(entry.Started) { - return errors.New(".Finished is not after .Started") - } - if entry.ResolverAddress != v.ExpectResolverAddress { - return errors.New("invalid .ResolverAddress value") - } - if entry.ResolverNetwork != v.ExpectResolverNetwork { - return errors.New("invalid .ResolverNetwork value") - } - return nil -} - -func TestSaverDNSRoundTrip(t *testing.T) { - // generateQueryAndReply generates a fake query and reply. - generateQueryAndReply := func() (query, reply []byte, err error) { - ff := &fakefill.Filler{} - ff.Fill(&query) - ff.Fill(&reply) - if len(query) < 1 || len(reply) < 1 { - return nil, nil, errors.New("did not generate query or reply") - } - return query, reply, nil - } - - // newDNSTransport creates a suitable DNSTransport. - newDNSTransport := func(reply []byte, err error) model.DNSTransport { - return &mocks.DNSTransport{ - MockRoundTrip: func(ctx context.Context, query []byte) ([]byte, error) { - return reply, err - }, - MockNetwork: func() string { - return "udp" - }, - MockAddress: func() string { - return "8.8.8.8:53" - }, - } - } - - t.Run("on success", func(t *testing.T) { - query, expectedReply, err := generateQueryAndReply() - if err != nil { - t.Fatal(err) - } - saver := NewSaver() - v := &SingleDNSRoundTripValidator{ - ExpectAddress: "8.8.8.8:53", - ExpectFailure: nil, - ExpectNetwork: "udp", - ExpectQuery: query, - ExpectReply: expectedReply, - Saver: saver, - } - ctx := context.Background() - txp := newDNSTransport(expectedReply, nil) - reply, err := saver.DNSRoundTrip(ctx, txp, query) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(expectedReply, reply); diff != "" { - t.Fatal(diff) - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - t.Run("on failure", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - query, _, err := generateQueryAndReply() - if err != nil { - t.Fatal(err) - } - saver := NewSaver() - v := &SingleDNSRoundTripValidator{ - ExpectAddress: "8.8.8.8:53", - ExpectFailure: mockedError, - ExpectNetwork: "udp", - ExpectQuery: query, - ExpectReply: nil, - Saver: saver, - } - ctx := context.Background() - txp := newDNSTransport(nil, mockedError) - reply, err := saver.DNSRoundTrip(ctx, txp, query) - if !errors.Is(err, mockedError) { - t.Fatal(err) - } - if len(reply) != 0 { - t.Fatal("unexpected reply") - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) -} - -type SingleDNSRoundTripValidator struct { - ExpectAddress string - ExpectFailure error - ExpectNetwork string - ExpectQuery []byte - ExpectReply []byte - Saver *Saver -} - -func (v *SingleDNSRoundTripValidator) Validate() error { - trace := v.Saver.MoveOutTrace() - if len(trace.DNSRoundTrip) != 1 { - return errors.New("expected a single entry") - } - entry := trace.DNSRoundTrip[0] - if v.ExpectAddress != entry.Address { - return errors.New("invalid .Address") - } - if !errors.Is(entry.Failure, v.ExpectFailure) { - return errors.New("invalid .Failure value") - } - if !entry.Finished.After(entry.Started) { - return errors.New(".Finished is not after .Started") - } - if v.ExpectNetwork != entry.Network { - return errors.New("invalid .Network value") - } - if diff := cmp.Diff(v.ExpectQuery, entry.Query); diff != "" { - return errors.New(diff) - } - if diff := cmp.Diff(v.ExpectReply, entry.Reply); diff != "" { - return errors.New(diff) - } - return nil -} diff --git a/internal/archival/saver.go b/internal/archival/saver.go deleted file mode 100644 index 2fcab4f..0000000 --- a/internal/archival/saver.go +++ /dev/null @@ -1,40 +0,0 @@ -package archival - -// -// Saver implementation -// - -import ( - "sync" -) - -// Saver allows to save network, DNS, QUIC, TLS, HTTP events. -// -// You MUST use NewSaver to create a new instance. -type Saver struct { - // mu provides mutual exclusion. - mu sync.Mutex - - // trace is the current trace. - trace *Trace -} - -// NewSaver creates a new Saver instance. -// -// You MUST use this function to create a Saver. -func NewSaver() *Saver { - return &Saver{ - mu: sync.Mutex{}, - trace: &Trace{}, - } -} - -// MoveOutTrace moves the current trace out of the saver and -// creates a new empty trace inside it. -func (as *Saver) MoveOutTrace() *Trace { - as.mu.Lock() - t := as.trace - as.trace = &Trace{} - as.mu.Unlock() - return t -} diff --git a/internal/archival/saver_test.go b/internal/archival/saver_test.go deleted file mode 100644 index 0c1ed99..0000000 --- a/internal/archival/saver_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package archival - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/fakefill" -) - -func TestSaverNewSaver(t *testing.T) { - saver := NewSaver() - if saver.trace == nil { - t.Fatal("expected non-nil trace here") - } -} - -func TestSaverMoveOutTrace(t *testing.T) { - saver := NewSaver() - var ev DNSRoundTripEvent - ff := &fakefill.Filler{} - ff.Fill(&ev) - if len(ev.Query) < 1 { - t.Fatal("did not fill") // be sure - } - saver.appendDNSRoundTripEvent(&ev) - trace := saver.MoveOutTrace() - if len(saver.trace.DNSRoundTrip) != 0 { - t.Fatal("expected zero length") - } - if len(trace.DNSRoundTrip) != 1 { - t.Fatal("expected one entry") - } - entry := trace.DNSRoundTrip[0] - if diff := cmp.Diff(&ev, entry); diff != "" { - t.Fatal(diff) - } -} diff --git a/internal/archival/tls.go b/internal/archival/tls.go deleted file mode 100644 index cf2b3a5..0000000 --- a/internal/archival/tls.go +++ /dev/null @@ -1,89 +0,0 @@ -package archival - -// -// Saves TLS events -// - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "net" - "time" - - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// QUICTLSHandshakeEvent contains a QUIC or TLS handshake event. -type QUICTLSHandshakeEvent struct { - ALPN []string - CipherSuite string - Failure error - Finished time.Time - NegotiatedProto string - Network string - PeerCerts [][]byte - RemoteAddr string - SNI string - SkipVerify bool - Started time.Time - TLSVersion string -} - -// TLSHandshake performs a TLS handshake with the given handshaker -// and saves the results into the saver. -func (s *Saver) TLSHandshake(ctx context.Context, thx model.TLSHandshaker, - conn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) { - network := conn.RemoteAddr().Network() - remoteAddr := conn.RemoteAddr().String() - started := time.Now() - tconn, state, err := thx.Handshake(ctx, conn, config) - // Implementation note: state is an empty ConnectionState on failure - // so it's safe to access its fields also in that case - s.appendTLSHandshake(&QUICTLSHandshakeEvent{ - ALPN: config.NextProtos, - CipherSuite: netxlite.TLSCipherSuiteString(state.CipherSuite), - Failure: err, - Finished: time.Now(), - NegotiatedProto: state.NegotiatedProtocol, - Network: network, - PeerCerts: s.tlsPeerCerts(err, &state), - RemoteAddr: remoteAddr, - SNI: config.ServerName, - SkipVerify: config.InsecureSkipVerify, - Started: started, - TLSVersion: netxlite.TLSVersionString(state.Version), - }) - return tconn, state, err -} - -func (s *Saver) appendTLSHandshake(ev *QUICTLSHandshakeEvent) { - s.mu.Lock() - s.trace.TLSHandshake = append(s.trace.TLSHandshake, ev) - s.mu.Unlock() -} - -func (s *Saver) tlsPeerCerts(err error, state *tls.ConnectionState) (out [][]byte) { - var x509HostnameError x509.HostnameError - if errors.As(err, &x509HostnameError) { - // Test case: https://wrong.host.badssl.com/ - return [][]byte{x509HostnameError.Certificate.Raw} - } - var x509UnknownAuthorityError x509.UnknownAuthorityError - if errors.As(err, &x509UnknownAuthorityError) { - // Test case: https://self-signed.badssl.com/. This error has - // never been among the ones returned by MK. - return [][]byte{x509UnknownAuthorityError.Cert.Raw} - } - var x509CertificateInvalidError x509.CertificateInvalidError - if errors.As(err, &x509CertificateInvalidError) { - // Test case: https://expired.badssl.com/ - return [][]byte{x509CertificateInvalidError.Cert.Raw} - } - for _, cert := range state.PeerCertificates { - out = append(out, cert.Raw) - } - return -} diff --git a/internal/archival/tls_test.go b/internal/archival/tls_test.go deleted file mode 100644 index ab9d602..0000000 --- a/internal/archival/tls_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package archival - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io" - "net" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/ooni/probe-cli/v3/internal/fakefill" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -func TestSaverTLSHandshake(t *testing.T) { - // newTLSHandshaker helps with building a TLS handshaker - newTLSHandshaker := func(tlsConn net.Conn, state tls.ConnectionState, err error) model.TLSHandshaker { - return &mocks.TLSHandshaker{ - MockHandshake: func(ctx context.Context, tcpConn net.Conn, config *tls.Config) (net.Conn, tls.ConnectionState, error) { - time.Sleep(1 * time.Microsecond) - return tlsConn, state, err - }, - } - } - - // newTCPConn creates a suitable net.Conn - newTCPConn := func(address string) net.Conn { - return &mocks.Conn{ - MockRemoteAddr: func() net.Addr { - return &mocks.Addr{ - MockString: func() string { - return address - }, - MockNetwork: func() string { - return "tcp" - }, - } - }, - MockClose: func() error { - return nil - }, - } - } - - t.Run("on success", func(t *testing.T) { - const mockedEndpoint = "8.8.4.4:443" - var certs [][]byte - ff := &fakefill.Filler{} - ff.Fill(&certs) - if len(certs) < 1 { - t.Fatal("did not fill certs") - } - saver := NewSaver() - v := &SingleQUICTLSHandshakeValidator{ - ExpectedALPN: []string{"h2", "http/1.1"}, - ExpectedSNI: "dns.google", - ExpectedSkipVerify: true, - // - ExpectedCipherSuite: tls.TLS_AES_128_GCM_SHA256, - ExpectedNegotiatedProtocol: "h2", - ExpectedPeerCerts: certs, - ExpectedVersion: tls.VersionTLS12, - // - ExpectedNetwork: "tcp", - ExpectedRemoteAddr: mockedEndpoint, - // - QUICConfig: nil, // this is not QUIC - ExpectedFailure: nil, - Saver: saver, - } - expectedState := v.NewTLSConnectionState() - thx := newTLSHandshaker(newTCPConn(mockedEndpoint), expectedState, nil) - ctx := context.Background() - tcpConn := newTCPConn(mockedEndpoint) - conn, state, err := saver.TLSHandshake(ctx, thx, tcpConn, v.NewTLSConfig()) - if conn == nil { - t.Fatal("expected non-nil conn") - } - conn.Close() - if diff := cmp.Diff(expectedState, state, cmpopts.IgnoreUnexported(tls.ConnectionState{})); diff != "" { - t.Fatal(diff) - } - if err != nil { - t.Fatal(err) - } - if err := v.Validate(); err != nil { - t.Fatal(err) - } - }) - - // failureFlow is the flow we run on failure. - failureFlow := func(mockedError error, peerCerts [][]byte) error { - const mockedEndpoint = "8.8.4.4:443" - saver := NewSaver() - v := &SingleQUICTLSHandshakeValidator{ - ExpectedALPN: []string{"h2", "http/1.1"}, - ExpectedSNI: "dns.google", - ExpectedSkipVerify: true, - // - ExpectedCipherSuite: 0, - ExpectedNegotiatedProtocol: "", - ExpectedPeerCerts: peerCerts, - ExpectedVersion: 0, - // - ExpectedNetwork: "tcp", - ExpectedRemoteAddr: mockedEndpoint, - // - QUICConfig: nil, // this is not QUIC - ExpectedFailure: mockedError, - Saver: saver, - } - expectedState := v.NewTLSConnectionState() - thx := newTLSHandshaker(nil, expectedState, mockedError) - ctx := context.Background() - tcpConn := newTCPConn(mockedEndpoint) - conn, state, err := saver.TLSHandshake(ctx, thx, tcpConn, v.NewTLSConfig()) - if conn != nil { - return errors.New("expected nil conn") - } - if diff := cmp.Diff(expectedState, state, cmpopts.IgnoreUnexported(tls.ConnectionState{})); diff != "" { - return errors.New(diff) - } - if !errors.Is(err, mockedError) { - return fmt.Errorf("unexpected err: %w", err) - } - return v.Validate() - } - - t.Run("on generic failure", func(t *testing.T) { - mockedError := netxlite.NewTopLevelGenericErrWrapper(io.EOF) - if err := failureFlow(mockedError, nil); err != nil { - t.Fatal(err) - } - }) - - t.Run("on x509.HostnameError", func(t *testing.T) { - var certificate []byte - ff := &fakefill.Filler{} - ff.Fill(&certificate) - mockedError := x509.HostnameError{ - Certificate: &x509.Certificate{Raw: certificate}, - } - if err := failureFlow(mockedError, [][]byte{certificate}); err != nil { - t.Fatal(err) - } - }) - - t.Run("on x509.UnknownAuthorityError", func(t *testing.T) { - var certificate []byte - ff := &fakefill.Filler{} - ff.Fill(&certificate) - mockedError := x509.UnknownAuthorityError{ - Cert: &x509.Certificate{Raw: certificate}, - } - if err := failureFlow(mockedError, [][]byte{certificate}); err != nil { - t.Fatal(err) - } - }) - - t.Run("on x509.CertificateInvalidError", func(t *testing.T) { - var certificate []byte - ff := &fakefill.Filler{} - ff.Fill(&certificate) - mockedError := x509.CertificateInvalidError{ - Cert: &x509.Certificate{Raw: certificate}, - } - if err := failureFlow(mockedError, [][]byte{certificate}); err != nil { - t.Fatal(err) - } - }) -} diff --git a/internal/archival/trace.go b/internal/archival/trace.go deleted file mode 100644 index 3c807ab..0000000 --- a/internal/archival/trace.go +++ /dev/null @@ -1,283 +0,0 @@ -package archival - -import ( - "net" - "net/http" - "sort" - "strconv" - "strings" - "time" - - "github.com/ooni/probe-cli/v3/internal/engine/geolocate" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// -// Trace implementation -// - -// Trace contains the events. -type Trace struct { - // DNSLookupHTTPS contains DNSLookupHTTPS events. - DNSLookupHTTPS []*DNSLookupEvent - - // DNSLookupHost contains DNSLookupHost events. - DNSLookupHost []*DNSLookupEvent - - // DNSRoundTrip contains DNSRoundTrip events. - DNSRoundTrip []*DNSRoundTripEvent - - // HTTPRoundTrip contains HTTPRoundTrip round trip events. - HTTPRoundTrip []*HTTPRoundTripEvent - - // Network contains network events. - Network []*NetworkEvent - - // QUICHandshake contains QUICHandshake handshake events. - QUICHandshake []*QUICTLSHandshakeEvent - - // TLSHandshake contains TLSHandshake handshake events. - TLSHandshake []*QUICTLSHandshakeEvent -} - -func (t *Trace) newFailure(err error) (out *string) { - if err != nil { - s := err.Error() - out = &s - } - return -} - -// -// TCP connect -// - -// NewArchivalTCPConnectResultList builds a TCP connect list in the OONI archival -// data format out of the results saved inside the trace. -func (t *Trace) NewArchivalTCPConnectResultList(begin time.Time) (out []model.ArchivalTCPConnectResult) { - for _, ev := range t.Network { - if ev.Operation != netxlite.ConnectOperation || ev.Network != "tcp" { - continue - } - // We assume Go is passing us legit data structures - ip, sport, _ := net.SplitHostPort(ev.RemoteAddr) - iport, _ := strconv.Atoi(sport) - out = append(out, model.ArchivalTCPConnectResult{ - IP: ip, - Port: iport, - Status: model.ArchivalTCPConnectStatus{ - Blocked: nil, // Web Connectivity only, depends on the control - Failure: t.newFailure(ev.Failure), - Success: ev.Failure == nil, - }, - T: ev.Finished.Sub(begin).Seconds(), - }) - } - return -} - -// -// HTTP -// - -// NewArchivalHTTPRequestResultList builds an HTTP requests list in the OONI -// archival data format out of the results saved inside the trace. -// -// This function will sort the emitted list of requests such that the last -// request that happened in time is the first one to be emitted. If the -// measurement code performs related requests sequentially (which is a kinda a -// given because you cannot follow a redirect before reading the previous request), -// then the result is sorted how the OONI pipeline expects it to be. -func (t *Trace) NewArchivalHTTPRequestResultList(begin time.Time) (out []model.ArchivalHTTPRequestResult) { - for _, ev := range t.HTTPRoundTrip { - out = append(out, model.ArchivalHTTPRequestResult{ - Failure: t.newFailure(ev.Failure), - Request: model.ArchivalHTTPRequest{ - Body: model.ArchivalMaybeBinaryData{}, - BodyIsTruncated: false, - HeadersList: t.newHTTPHeadersList(ev.RequestHeaders), - Headers: t.newHTTPHeadersMap(ev.RequestHeaders), - Method: ev.Method, - Tor: model.ArchivalHTTPTor{}, - Transport: ev.Transport, - URL: ev.URL, - }, - Response: model.ArchivalHTTPResponse{ - Body: model.ArchivalMaybeBinaryData{ - Value: string(ev.ResponseBody), - }, - BodyIsTruncated: ev.ResponseBodyIsTruncated, - Code: ev.StatusCode, - HeadersList: t.newHTTPHeadersList(ev.ResponseHeaders), - Headers: t.newHTTPHeadersMap(ev.ResponseHeaders), - Locations: ev.ResponseHeaders.Values("Location"), // safe with nil headers - }, - T: ev.Finished.Sub(begin).Seconds(), - }) - } - // Implementation note: historically OONI has always added - // the _last_ measurement in _first_ position. This has only - // been relevant for sequentially performed requests. For - // this purpose it feels okay to use T as the sorting key, - // since it's the time when we exited RoundTrip(). - sort.Slice(out, func(i, j int) bool { - return out[i].T > out[j].T - }) - return -} - -func (t *Trace) newHTTPHeadersList(source http.Header) (out []model.ArchivalHTTPHeader) { - for key, values := range source { - for _, value := range values { - out = append(out, model.ArchivalHTTPHeader{ - Key: key, - Value: model.ArchivalMaybeBinaryData{ - Value: value, - }, - }) - } - } - // Implementation note: we need to sort the keys to have - // stable testing since map iteration is random. - sort.Slice(out, func(i, j int) bool { - return out[i].Key < out[j].Key - }) - return -} - -func (t *Trace) newHTTPHeadersMap(source http.Header) (out map[string]model.ArchivalMaybeBinaryData) { - for key, values := range source { - for index, value := range values { - if index > 0 { - break // only the first entry - } - if out == nil { - out = make(map[string]model.ArchivalMaybeBinaryData) - } - out[key] = model.ArchivalMaybeBinaryData{Value: value} - } - } - return -} - -// -// DNS -// - -// NewArchivalDNSLookupResultList builds a DNS lookups list in the OONI -// archival data format out of the results saved inside the trace. -func (t *Trace) NewArchivalDNSLookupResultList(begin time.Time) (out []model.ArchivalDNSLookupResult) { - for _, ev := range t.DNSLookupHost { - out = append(out, model.ArchivalDNSLookupResult{ - Answers: t.gatherA(ev.Addresses), - Engine: ev.ResolverNetwork, - Failure: t.newFailure(ev.Failure), - Hostname: ev.Domain, - QueryType: "A", - ResolverHostname: nil, // legacy - ResolverPort: nil, // legacy - ResolverAddress: ev.ResolverAddress, - T: ev.Finished.Sub(begin).Seconds(), - }) - aaaa := t.gatherAAAA(ev.Addresses) - if len(aaaa) <= 0 && ev.Failure == nil { - // We don't have any AAAA results. Historically we do not - // create a record for AAAA with no results when A succeeded - continue - } - out = append(out, model.ArchivalDNSLookupResult{ - Answers: aaaa, - Engine: ev.ResolverNetwork, - Failure: t.newFailure(ev.Failure), - Hostname: ev.Domain, - QueryType: "AAAA", - ResolverHostname: nil, // legacy - ResolverPort: nil, // legacy - ResolverAddress: ev.ResolverAddress, - T: ev.Finished.Sub(begin).Seconds(), - }) - } - return -} - -func (t *Trace) gatherA(addrs []string) (out []model.ArchivalDNSAnswer) { - for _, addr := range addrs { - if strings.Contains(addr, ":") { - continue // it's AAAA so we need to skip it - } - answer := model.ArchivalDNSAnswer{AnswerType: "A"} - asn, org, _ := geolocate.LookupASN(addr) - answer.ASN = int64(asn) - answer.ASOrgName = org - answer.IPv4 = addr - out = append(out, answer) - } - return -} - -func (t *Trace) gatherAAAA(addrs []string) (out []model.ArchivalDNSAnswer) { - for _, addr := range addrs { - if !strings.Contains(addr, ":") { - continue // it's A so we need to skip it - } - answer := model.ArchivalDNSAnswer{AnswerType: "AAAA"} - asn, org, _ := geolocate.LookupASN(addr) - answer.ASN = int64(asn) - answer.ASOrgName = org - answer.IPv6 = addr - out = append(out, answer) - } - return -} - -// -// NetworkEvents -// - -// NewArchivalNetworkEventList builds a network events list in the OONI -// archival data format out of the results saved inside the trace. -func (t *Trace) NewArchivalNetworkEventList(begin time.Time) (out []model.ArchivalNetworkEvent) { - for _, ev := range t.Network { - out = append(out, model.ArchivalNetworkEvent{ - Address: ev.RemoteAddr, - Failure: t.newFailure(ev.Failure), - NumBytes: int64(ev.Count), - Operation: ev.Operation, - Proto: ev.Network, - T: ev.Finished.Sub(begin).Seconds(), - Tags: nil, - }) - } - return -} - -// -// TLS handshake -// - -// NewArchivalTLSHandshakeResultList builds a TLS handshakes list in the OONI -// archival data format out of the results saved inside the trace. -func (t *Trace) NewArchivalTLSHandshakeResultList(begin time.Time) (out []model.ArchivalTLSOrQUICHandshakeResult) { - for _, ev := range t.TLSHandshake { - out = append(out, model.ArchivalTLSOrQUICHandshakeResult{ - CipherSuite: ev.CipherSuite, - Failure: t.newFailure(ev.Failure), - NegotiatedProtocol: ev.NegotiatedProto, - NoTLSVerify: ev.SkipVerify, - PeerCertificates: t.makePeerCerts(ev.PeerCerts), - ServerName: ev.SNI, - T: ev.Finished.Sub(begin).Seconds(), - Tags: nil, - TLSVersion: ev.TLSVersion, - }) - } - return -} - -func (t *Trace) makePeerCerts(in [][]byte) (out []model.ArchivalMaybeBinaryData) { - for _, v := range in { - out = append(out, model.ArchivalMaybeBinaryData{Value: string(v)}) - } - return -} diff --git a/internal/archival/trace_test.go b/internal/archival/trace_test.go deleted file mode 100644 index b02dd18..0000000 --- a/internal/archival/trace_test.go +++ /dev/null @@ -1,921 +0,0 @@ -package archival - -import ( - "errors" - "io" - "net/http" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// traceTime generates a time that is based off a fixed beginning -// in time, so we can easily compare times. -func traceTime(d int64) time.Time { - t := time.Date(2021, 01, 13, 14, 21, 59, 0, time.UTC) - return t.Add(time.Duration(d) * time.Millisecond) -} - -// deltaSinceTraceTime computes the delta since the original -// trace time expressed in floating point seconds. -func deltaSinceTraceTime(d int64) float64 { - return (time.Duration(d) * time.Millisecond).Seconds() -} - -// failureFromString converts a string to a failure. -func failureFromString(failure string) *string { - return &failure -} - -func TestTraceNewArchivalTCPConnectResultList(t *testing.T) { - type fields struct { - DNSLookupHTTPS []*DNSLookupEvent - DNSLookupHost []*DNSLookupEvent - DNSRoundTrip []*DNSRoundTripEvent - HTTPRoundTrip []*HTTPRoundTripEvent - Network []*NetworkEvent - QUICHandshake []*QUICTLSHandshakeEvent - TLSHandshake []*QUICTLSHandshakeEvent - } - type args struct { - begin time.Time - } - tests := []struct { - name string - fields fields - args args - wantOut []model.ArchivalTCPConnectResult - }{{ - name: "with empty trace", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "we ignore I/O operations", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{{ - Count: 1024, - Failure: nil, - Finished: traceTime(2), - Network: "tcp", - Operation: netxlite.WriteOperation, - RemoteAddr: "8.8.8.8:443", - Started: traceTime(1), - }, { - Count: 4096, - Failure: nil, - Finished: traceTime(4), - Network: "tcp", - Operation: netxlite.ReadOperation, - RemoteAddr: "8.8.8.8:443", - Started: traceTime(3), - }}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "we ignore UDP connect", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{{ - Count: 0, - Failure: nil, - Finished: traceTime(2), - Network: "udp", - Operation: netxlite.ConnectOperation, - RemoteAddr: "8.8.8.8:53", - Started: traceTime(1), - }}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "with TCP connect success", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{{ - Count: 0, - Failure: nil, - Finished: traceTime(2), - Network: "tcp", - Operation: netxlite.ConnectOperation, - RemoteAddr: "8.8.8.8:443", - Started: traceTime(1), - }}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalTCPConnectResult{{ - IP: "8.8.8.8", - Port: 443, - Status: model.ArchivalTCPConnectStatus{ - Blocked: nil, - Failure: nil, - Success: true, - }, - T: deltaSinceTraceTime(2), - }}, - }, { - name: "with TCP connect failure", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{{ - Count: 0, - Failure: netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNREFUSED), - Finished: traceTime(2), - Network: "tcp", - Operation: netxlite.ConnectOperation, - RemoteAddr: "8.8.8.8:443", - Started: traceTime(1), - }}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalTCPConnectResult{{ - IP: "8.8.8.8", - Port: 443, - Status: model.ArchivalTCPConnectStatus{ - Blocked: nil, - Failure: failureFromString(netxlite.FailureConnectionRefused), - Success: false, - }, - T: deltaSinceTraceTime(2), - }}, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &Trace{ - DNSLookupHTTPS: tt.fields.DNSLookupHTTPS, - DNSLookupHost: tt.fields.DNSLookupHost, - DNSRoundTrip: tt.fields.DNSRoundTrip, - HTTPRoundTrip: tt.fields.HTTPRoundTrip, - Network: tt.fields.Network, - QUICHandshake: tt.fields.QUICHandshake, - TLSHandshake: tt.fields.TLSHandshake, - } - gotOut := tr.NewArchivalTCPConnectResultList(tt.args.begin) - if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestTraceNewArchivalHTTPRequestResultList(t *testing.T) { - type fields struct { - DNSLookupHTTPS []*DNSLookupEvent - DNSLookupHost []*DNSLookupEvent - DNSRoundTrip []*DNSRoundTripEvent - HTTPRoundTrip []*HTTPRoundTripEvent - Network []*NetworkEvent - QUICHandshake []*QUICTLSHandshakeEvent - TLSHandshake []*QUICTLSHandshakeEvent - } - type args struct { - begin time.Time - } - tests := []struct { - name string - fields fields - args args - wantOut []model.ArchivalHTTPRequestResult - }{{ - name: "with empty trace", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "with failure", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{{ - Failure: netxlite.NewTopLevelGenericErrWrapper(netxlite.ECONNRESET), - Finished: traceTime(2), - Method: "GET", - RequestHeaders: http.Header{ - "Accept": {"*/*"}, - "X-Cookie": {"A", "B", "C"}, - }, - ResponseBody: nil, - ResponseBodyIsTruncated: false, - ResponseBodyLength: 0, - ResponseHeaders: nil, - Started: traceTime(1), - StatusCode: 0, - Transport: "tcp", - URL: "http://x.org/", - }}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalHTTPRequestResult{{ - Failure: failureFromString(netxlite.FailureConnectionReset), - Request: model.ArchivalHTTPRequest{ - Body: model.ArchivalMaybeBinaryData{}, - BodyIsTruncated: false, - HeadersList: []model.ArchivalHTTPHeader{{ - Key: "Accept", - Value: model.ArchivalMaybeBinaryData{ - Value: "*/*", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "A", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "B", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "C", - }, - }}, - Headers: map[string]model.ArchivalMaybeBinaryData{ - "Accept": {Value: "*/*"}, - "X-Cookie": {Value: "A"}, - }, - Method: "GET", - Tor: model.ArchivalHTTPTor{}, - Transport: "tcp", - URL: "http://x.org/", - }, - Response: model.ArchivalHTTPResponse{ - Body: model.ArchivalMaybeBinaryData{}, - BodyIsTruncated: false, - Code: 0, - HeadersList: nil, - Headers: nil, - Locations: nil, - }, - T: deltaSinceTraceTime(2), - }}, - }, { - name: "with success", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{{ - Failure: nil, - Finished: traceTime(2), - Method: "GET", - RequestHeaders: http.Header{ - "Accept": {"*/*"}, - "X-Cookie": {"A", "B", "C"}, - }, - ResponseBody: []byte("0xdeadbeef"), - ResponseBodyIsTruncated: true, - ResponseBodyLength: 10, - ResponseHeaders: http.Header{ - "Server": {"antani/1.0"}, - "X-Cookie-Reply": {"C", "D", "F"}, - "Location": {"https://x.org/", "https://x.org/robots.txt"}, - }, - Started: traceTime(1), - StatusCode: 302, - Transport: "tcp", - URL: "http://x.org/", - }}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalHTTPRequestResult{{ - Failure: nil, - Request: model.ArchivalHTTPRequest{ - Body: model.ArchivalMaybeBinaryData{}, - BodyIsTruncated: false, - HeadersList: []model.ArchivalHTTPHeader{{ - Key: "Accept", - Value: model.ArchivalMaybeBinaryData{ - Value: "*/*", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "A", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "B", - }, - }, { - Key: "X-Cookie", - Value: model.ArchivalMaybeBinaryData{ - Value: "C", - }, - }}, - Headers: map[string]model.ArchivalMaybeBinaryData{ - "Accept": {Value: "*/*"}, - "X-Cookie": {Value: "A"}, - }, - Method: "GET", - Tor: model.ArchivalHTTPTor{}, - Transport: "tcp", - URL: "http://x.org/", - }, - Response: model.ArchivalHTTPResponse{ - Body: model.ArchivalMaybeBinaryData{ - Value: "0xdeadbeef", - }, - BodyIsTruncated: true, - Code: 302, - HeadersList: []model.ArchivalHTTPHeader{{ - Key: "Location", - Value: model.ArchivalMaybeBinaryData{ - Value: "https://x.org/", - }, - }, { - Key: "Location", - Value: model.ArchivalMaybeBinaryData{ - Value: "https://x.org/robots.txt", - }, - }, { - Key: "Server", - Value: model.ArchivalMaybeBinaryData{ - Value: "antani/1.0", - }, - }, { - Key: "X-Cookie-Reply", - Value: model.ArchivalMaybeBinaryData{ - Value: "C", - }, - }, { - Key: "X-Cookie-Reply", - Value: model.ArchivalMaybeBinaryData{ - Value: "D", - }, - }, { - Key: "X-Cookie-Reply", - Value: model.ArchivalMaybeBinaryData{ - Value: "F", - }, - }}, - Headers: map[string]model.ArchivalMaybeBinaryData{ - "Server": {Value: "antani/1.0"}, - "X-Cookie-Reply": {Value: "C"}, - "Location": {Value: "https://x.org/"}, - }, - Locations: []string{ - "https://x.org/", - "https://x.org/robots.txt", - }, - }, - T: deltaSinceTraceTime(2), - }}, - }, { - name: "The result is sorted by the value of T", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{{ - Failure: nil, - Finished: traceTime(3), - Method: "", - RequestHeaders: map[string][]string{}, - ResponseBody: []byte{}, - ResponseBodyIsTruncated: false, - ResponseBodyLength: 0, - ResponseHeaders: map[string][]string{}, - Started: time.Time{}, - StatusCode: 0, - Transport: "", - URL: "", - }, { - Failure: nil, - Finished: traceTime(2), - Method: "", - RequestHeaders: map[string][]string{}, - ResponseBody: []byte{}, - ResponseBodyIsTruncated: false, - ResponseBodyLength: 0, - ResponseHeaders: map[string][]string{}, - Started: time.Time{}, - StatusCode: 0, - Transport: "", - URL: "", - }, { - Failure: nil, - Finished: traceTime(5), - Method: "", - RequestHeaders: map[string][]string{}, - ResponseBody: []byte{}, - ResponseBodyIsTruncated: false, - ResponseBodyLength: 0, - ResponseHeaders: map[string][]string{}, - Started: time.Time{}, - StatusCode: 0, - Transport: "", - URL: "", - }}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalHTTPRequestResult{{ - Failure: nil, - Request: model.ArchivalHTTPRequest{}, - Response: model.ArchivalHTTPResponse{}, - T: deltaSinceTraceTime(5), - }, { - Failure: nil, - Request: model.ArchivalHTTPRequest{}, - Response: model.ArchivalHTTPResponse{}, - T: deltaSinceTraceTime(3), - }, { - Failure: nil, - Request: model.ArchivalHTTPRequest{}, - Response: model.ArchivalHTTPResponse{}, - T: deltaSinceTraceTime(2), - }}, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &Trace{ - DNSLookupHTTPS: tt.fields.DNSLookupHTTPS, - DNSLookupHost: tt.fields.DNSLookupHost, - DNSRoundTrip: tt.fields.DNSRoundTrip, - HTTPRoundTrip: tt.fields.HTTPRoundTrip, - Network: tt.fields.Network, - QUICHandshake: tt.fields.QUICHandshake, - TLSHandshake: tt.fields.TLSHandshake, - } - gotOut := tr.NewArchivalHTTPRequestResultList(tt.args.begin) - if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestTraceNewArchivalDNSLookupResultList(t *testing.T) { - type fields struct { - DNSLookupHTTPS []*DNSLookupEvent - DNSLookupHost []*DNSLookupEvent - DNSRoundTrip []*DNSRoundTripEvent - HTTPRoundTrip []*HTTPRoundTripEvent - Network []*NetworkEvent - QUICHandshake []*QUICTLSHandshakeEvent - TLSHandshake []*QUICTLSHandshakeEvent - } - type args struct { - begin time.Time - } - tests := []struct { - name string - fields fields - args args - wantOut []model.ArchivalDNSLookupResult - }{{ - name: "with empty trace", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "with NXDOMAIN failure", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{{ - ALPNs: nil, - Addresses: nil, - Domain: "example.com", - Failure: netxlite.NewTopLevelGenericErrWrapper(errors.New(netxlite.DNSNoSuchHostSuffix)), - Finished: traceTime(2), - LookupType: "", // not processed - ResolverAddress: "8.8.8.8:53", - ResolverNetwork: "udp", - Started: traceTime(1), - }}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalDNSLookupResult{{ - Answers: nil, - Engine: "udp", - Failure: failureFromString(netxlite.FailureDNSNXDOMAINError), - Hostname: "example.com", - QueryType: "A", - ResolverHostname: nil, - ResolverPort: nil, - ResolverAddress: "8.8.8.8:53", - T: deltaSinceTraceTime(2), - }, { - Answers: nil, - Engine: "udp", - Failure: failureFromString(netxlite.FailureDNSNXDOMAINError), - Hostname: "example.com", - QueryType: "AAAA", - ResolverHostname: nil, - ResolverPort: nil, - ResolverAddress: "8.8.8.8:53", - T: deltaSinceTraceTime(2), - }}, - }, { - name: "with success for A and AAAA", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{{ - ALPNs: nil, - Addresses: []string{ - "8.8.8.8", "8.8.4.4", "2001:4860:4860::8844", - }, - Domain: "dns.google", - Failure: nil, - Finished: traceTime(2), - LookupType: "", // not processed - ResolverAddress: "8.8.8.8:53", - ResolverNetwork: "udp", - Started: traceTime(1), - }}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalDNSLookupResult{{ - Answers: []model.ArchivalDNSAnswer{{ - ASN: 15169, - ASOrgName: "Google LLC", - AnswerType: "A", - Hostname: "", - IPv4: "8.8.8.8", - IPv6: "", - TTL: nil, - }, { - ASN: 15169, - ASOrgName: "Google LLC", - AnswerType: "A", - Hostname: "", - IPv4: "8.8.4.4", - IPv6: "", - TTL: nil, - }}, - Engine: "udp", - Failure: nil, - Hostname: "dns.google", - QueryType: "A", - ResolverHostname: nil, - ResolverPort: nil, - ResolverAddress: "8.8.8.8:53", - T: deltaSinceTraceTime(2), - }, { - Answers: []model.ArchivalDNSAnswer{{ - ASN: 15169, - ASOrgName: "Google LLC", - AnswerType: "AAAA", - Hostname: "", - IPv4: "", - IPv6: "2001:4860:4860::8844", - TTL: nil, - }}, - Engine: "udp", - Failure: nil, - Hostname: "dns.google", - QueryType: "AAAA", - ResolverHostname: nil, - ResolverPort: nil, - ResolverAddress: "8.8.8.8:53", - T: deltaSinceTraceTime(2), - }}, - }, { - name: "when a domain has no AAAA addresses", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{{ - ALPNs: nil, - Addresses: []string{ - "8.8.8.8", "8.8.4.4", - }, - Domain: "dns.google", - Failure: nil, - Finished: traceTime(2), - LookupType: "", // not processed - ResolverAddress: "8.8.8.8:53", - ResolverNetwork: "udp", - Started: traceTime(1), - }}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalDNSLookupResult{{ - Answers: []model.ArchivalDNSAnswer{{ - ASN: 15169, - ASOrgName: "Google LLC", - AnswerType: "A", - Hostname: "", - IPv4: "8.8.8.8", - IPv6: "", - TTL: nil, - }, { - ASN: 15169, - ASOrgName: "Google LLC", - AnswerType: "A", - Hostname: "", - IPv4: "8.8.4.4", - IPv6: "", - TTL: nil, - }}, - Engine: "udp", - Failure: nil, - Hostname: "dns.google", - QueryType: "A", - ResolverHostname: nil, - ResolverPort: nil, - ResolverAddress: "8.8.8.8:53", - T: deltaSinceTraceTime(2), - }}, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &Trace{ - DNSLookupHTTPS: tt.fields.DNSLookupHTTPS, - DNSLookupHost: tt.fields.DNSLookupHost, - DNSRoundTrip: tt.fields.DNSRoundTrip, - HTTPRoundTrip: tt.fields.HTTPRoundTrip, - Network: tt.fields.Network, - QUICHandshake: tt.fields.QUICHandshake, - TLSHandshake: tt.fields.TLSHandshake, - } - gotOut := tr.NewArchivalDNSLookupResultList(tt.args.begin) - if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestTraceNewArchivalNetworkEventList(t *testing.T) { - type fields struct { - DNSLookupHTTPS []*DNSLookupEvent - DNSLookupHost []*DNSLookupEvent - DNSRoundTrip []*DNSRoundTripEvent - HTTPRoundTrip []*HTTPRoundTripEvent - Network []*NetworkEvent - QUICHandshake []*QUICTLSHandshakeEvent - TLSHandshake []*QUICTLSHandshakeEvent - } - type args struct { - begin time.Time - } - tests := []struct { - name string - fields fields - args args - wantOut []model.ArchivalNetworkEvent - }{{ - name: "with empty trace", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "we fill all the fields we should be filling", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{{ - Count: 1234, - Failure: netxlite.NewTopLevelGenericErrWrapper(io.EOF), - Finished: traceTime(2), - Network: "tcp", - Operation: netxlite.ReadOperation, - RemoteAddr: "8.8.8.8:443", - Started: traceTime(1), - }}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalNetworkEvent{{ - Address: "8.8.8.8:443", - Failure: failureFromString(netxlite.FailureEOFError), - NumBytes: 1234, - Operation: netxlite.ReadOperation, - Proto: "tcp", - T: deltaSinceTraceTime(2), - Tags: nil, - }}, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &Trace{ - DNSLookupHTTPS: tt.fields.DNSLookupHTTPS, - DNSLookupHost: tt.fields.DNSLookupHost, - DNSRoundTrip: tt.fields.DNSRoundTrip, - HTTPRoundTrip: tt.fields.HTTPRoundTrip, - Network: tt.fields.Network, - QUICHandshake: tt.fields.QUICHandshake, - TLSHandshake: tt.fields.TLSHandshake, - } - gotOut := tr.NewArchivalNetworkEventList(tt.args.begin) - if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" { - t.Fatal(diff) - } - }) - } -} - -func TestTraceNewArchivalTLSHandshakeResultList(t *testing.T) { - type fields struct { - DNSLookupHTTPS []*DNSLookupEvent - DNSLookupHost []*DNSLookupEvent - DNSRoundTrip []*DNSRoundTripEvent - HTTPRoundTrip []*HTTPRoundTripEvent - Network []*NetworkEvent - QUICHandshake []*QUICTLSHandshakeEvent - TLSHandshake []*QUICTLSHandshakeEvent - } - type args struct { - begin time.Time - } - tests := []struct { - name string - fields fields - args args - wantOut []model.ArchivalTLSOrQUICHandshakeResult - }{{ - name: "with empty trace", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: nil, - }, { - name: "we fill all the fields we should be filling", - fields: fields{ - DNSLookupHTTPS: []*DNSLookupEvent{}, - DNSLookupHost: []*DNSLookupEvent{}, - DNSRoundTrip: []*DNSRoundTripEvent{}, - HTTPRoundTrip: []*HTTPRoundTripEvent{}, - Network: []*NetworkEvent{}, - QUICHandshake: []*QUICTLSHandshakeEvent{}, - TLSHandshake: []*QUICTLSHandshakeEvent{{ - ALPN: []string{"h2", "http/1.1"}, - CipherSuite: "TLS_AES_128_GCM_SHA256", - Failure: netxlite.NewTopLevelGenericErrWrapper(io.EOF), - Finished: traceTime(2), - NegotiatedProto: "h2", - Network: "tcp", - PeerCerts: [][]byte{ - []byte("deadbeef"), - []byte("xox"), - }, - RemoteAddr: "8.8.8.8:443", - SNI: "dns.google", - SkipVerify: true, - Started: traceTime(1), - TLSVersion: "TLSv1.3", - }}, - }, - args: args{ - begin: traceTime(0), - }, - wantOut: []model.ArchivalTLSOrQUICHandshakeResult{{ - CipherSuite: "TLS_AES_128_GCM_SHA256", - Failure: failureFromString(netxlite.FailureEOFError), - NegotiatedProtocol: "h2", - NoTLSVerify: true, - PeerCertificates: []model.ArchivalMaybeBinaryData{{ - Value: "deadbeef", - }, { - Value: "xox", - }}, - ServerName: "dns.google", - T: deltaSinceTraceTime(2), - Tags: nil, - TLSVersion: "TLSv1.3", - }}, - }} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tr := &Trace{ - DNSLookupHTTPS: tt.fields.DNSLookupHTTPS, - DNSLookupHost: tt.fields.DNSLookupHost, - DNSRoundTrip: tt.fields.DNSRoundTrip, - HTTPRoundTrip: tt.fields.HTTPRoundTrip, - Network: tt.fields.Network, - QUICHandshake: tt.fields.QUICHandshake, - TLSHandshake: tt.fields.TLSHandshake, - } - gotOut := tr.NewArchivalTLSHandshakeResultList(tt.args.begin) - if diff := cmp.Diff(tt.wantOut, gotOut); diff != "" { - t.Fatal(diff) - } - }) - } -} diff --git a/internal/cmd/miniooni/libminiooni.go b/internal/cmd/miniooni/libminiooni.go index 5844c0c..08c7d82 100644 --- a/internal/cmd/miniooni/libminiooni.go +++ b/internal/cmd/miniooni/libminiooni.go @@ -20,9 +20,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/kvstore" "github.com/ooni/probe-cli/v3/internal/legacy/assetsdir" "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/netxlite/filtering" - "github.com/ooni/probe-cli/v3/internal/runtimex" "github.com/ooni/probe-cli/v3/internal/version" "github.com/pborman/getopt/v2" ) @@ -30,7 +27,6 @@ import ( // Options contains the options you can set from the CLI. type Options struct { Annotations []string - Censor string ExtraOptions []string HomeDir string Inputs []string @@ -65,10 +61,6 @@ func init() { getopt.FlagLong( &globalOptions.Annotations, "annotation", 'A', "Add annotaton", "KEY=VALUE", ) - getopt.FlagLong( - &globalOptions.Censor, "censor", 0, - "Specifies censorship rules to apply for QA purposes", "FILE", - ) getopt.FlagLong( &globalOptions.ExtraOptions, "option", 'O', "Pass an option to the experiment", "KEY=VALUE", @@ -316,17 +308,6 @@ func MainWithConfiguration(experimentName string, currentOptions Options) { } log.Log = logger - if currentOptions.Censor != "" { - config, err := filtering.NewTProxyConfig(currentOptions.Censor) - runtimex.PanicOnError(err, "cannot parse --censor file as JSON") - tproxy, err := filtering.NewTProxy(config, log.Log) - runtimex.PanicOnError(err, "cannot create tproxy instance") - defer tproxy.Close() - netxlite.TProxy = tproxy - log.Infof("miniooni: disabling submission with --censor to avoid pulluting OONI data") - currentOptions.NoCollector = true - } - //Mon Jan 2 15:04:05 -0700 MST 2006 log.Infof("Current time: %s", time.Now().Format("2006-01-02 15:04:05 MST")) diff --git a/internal/cmd/oohelper/oohelper.go b/internal/cmd/oohelper/oohelper.go index 40c2c34..3dda531 100644 --- a/internal/cmd/oohelper/oohelper.go +++ b/internal/cmd/oohelper/oohelper.go @@ -11,9 +11,7 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/cmd/oohelper/internal" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx" "github.com/ooni/probe-cli/v3/internal/engine/netx" - "github.com/ooni/probe-cli/v3/internal/measurex" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/runtimex" ) @@ -25,7 +23,6 @@ var ( resolver model.Resolver server = flag.String("server", "", "URL of the test helper") target = flag.String("target", "", "Target URL for the test helper") - fwebsteps = flag.Bool("websteps", false, "Use the websteps TH") ) func newhttpclient() *http.Client { @@ -54,34 +51,12 @@ func main() { } flag.Parse() log.SetLevel(logmap[*debug]) - apimap := map[bool]func() interface{}{ - false: wcth, - true: webstepsth, - } - cresp := apimap[*fwebsteps]() + cresp := wcth() data, err := json.MarshalIndent(cresp, "", " ") runtimex.PanicOnError(err, "json.MarshalIndent failed") fmt.Printf("%s\n", string(data)) } -func webstepsth() interface{} { - serverURL := *server - if serverURL == "" { - serverURL = "https://1.th.ooni.org/api/v1/websteps" - } - clnt := &webstepsx.THClient{ - DNServers: []*measurex.ResolverInfo{{ - Network: "udp", - Address: "8.8.4.4:53", - }}, - HTTPClient: httpClient, - ServerURL: serverURL, - } - cresp, err := clnt.Run(ctx, *target) - runtimex.PanicOnError(err, "client.Run failed") - return cresp -} - func wcth() interface{} { serverURL := *server if serverURL == "" { diff --git a/internal/cmd/oohelperd/oohelperd.go b/internal/cmd/oohelperd/oohelperd.go index 631ce4c..42ad103 100644 --- a/internal/cmd/oohelperd/oohelperd.go +++ b/internal/cmd/oohelperd/oohelperd.go @@ -10,7 +10,6 @@ import ( "github.com/apex/log" "github.com/ooni/probe-cli/v3/internal/cmd/oohelperd/internal/webconnectivity" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx" "github.com/ooni/probe-cli/v3/internal/engine/netx" "github.com/ooni/probe-cli/v3/internal/model" "github.com/ooni/probe-cli/v3/internal/runtimex" @@ -58,7 +57,6 @@ func main() { func testableMain() { mux := http.NewServeMux() - mux.Handle("/api/v1/websteps", &webstepsx.THHandler{}) mux.Handle("/", webconnectivity.Handler{ Client: httpx, Dialer: dialer, diff --git a/internal/engine/allexperiments.go b/internal/engine/allexperiments.go index 20a7c34..f702118 100644 --- a/internal/engine/allexperiments.go +++ b/internal/engine/allexperiments.go @@ -29,7 +29,6 @@ import ( "github.com/ooni/probe-cli/v3/internal/engine/experiment/urlgetter" "github.com/ooni/probe-cli/v3/internal/engine/experiment/vanillator" "github.com/ooni/probe-cli/v3/internal/engine/experiment/webconnectivity" - "github.com/ooni/probe-cli/v3/internal/engine/experiment/webstepsx" "github.com/ooni/probe-cli/v3/internal/engine/experiment/whatsapp" ) @@ -352,18 +351,6 @@ var experimentsByName = map[string]func(*Session) *ExperimentBuilder{ } }, - "websteps": func(session *Session) *ExperimentBuilder { - return &ExperimentBuilder{ - build: func(config interface{}) *Experiment { - return NewExperiment(session, webstepsx.NewExperimentMeasurer( - *config.(*webstepsx.Config), - )) - }, - config: &webstepsx.Config{}, - inputPolicy: InputOrQueryBackend, - } - }, - "whatsapp": func(session *Session) *ExperimentBuilder { return &ExperimentBuilder{ build: func(config interface{}) *Experiment { diff --git a/internal/engine/experiment/webstepsx/doc.go b/internal/engine/experiment/webstepsx/doc.go deleted file mode 100644 index 5c3af36..0000000 --- a/internal/engine/experiment/webstepsx/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package webstepsx contains a websteps implementation -// based on the internal/measurex package. -// -// This implementation does not follow any existing spec -// rather we are modeling the spec on this one. -package webstepsx diff --git a/internal/engine/experiment/webstepsx/measurer.go b/internal/engine/experiment/webstepsx/measurer.go deleted file mode 100644 index 7b4cb58..0000000 --- a/internal/engine/experiment/webstepsx/measurer.go +++ /dev/null @@ -1,223 +0,0 @@ -package webstepsx - -// -// Measurer -// -// This file contains the client implementation. -// - -import ( - "context" - "errors" - "net/http" - "net/url" - "time" - - "github.com/ooni/probe-cli/v3/internal/engine/netx/archival" - "github.com/ooni/probe-cli/v3/internal/measurex" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -const ( - testName = "websteps" - testVersion = "0.0.3" -) - -// Config contains the experiment config. -type Config struct{} - -// TestKeys contains the experiment's test keys. -type TestKeys struct { - *measurex.ArchivalURLMeasurement -} - -// Measurer performs the measurement. -type Measurer struct { - Config Config -} - -var ( - _ model.ExperimentMeasurer = &Measurer{} - _ model.ExperimentMeasurerAsync = &Measurer{} -) - -// NewExperimentMeasurer creates a new ExperimentMeasurer. -func NewExperimentMeasurer(config Config) model.ExperimentMeasurer { - return &Measurer{Config: config} -} - -// ExperimentName implements ExperimentMeasurer.ExperExperimentName. -func (mx *Measurer) ExperimentName() string { - return testName -} - -// ExperimentVersion implements ExperimentMeasurer.ExperExperimentVersion. -func (mx *Measurer) ExperimentVersion() string { - return testVersion -} - -var ( - // ErrNoAvailableTestHelpers is emitted when there are no available test helpers. - ErrNoAvailableTestHelpers = errors.New("no available helpers") - - // ErrNoInput indicates that no input was provided. - ErrNoInput = errors.New("no input provided") - - // ErrInputIsNotAnURL indicates that the input is not an URL. - ErrInputIsNotAnURL = errors.New("input is not an URL") - - // ErrUnsupportedInput indicates that the input URL scheme is unsupported. - ErrUnsupportedInput = errors.New("unsupported input scheme") -) - -// RunAsync implements ExperimentMeasurerAsync.RunAsync. -func (mx *Measurer) RunAsync( - ctx context.Context, sess model.ExperimentSession, input string, - callbacks model.ExperimentCallbacks) (<-chan *model.ExperimentAsyncTestKeys, error) { - // 1. Parse and verify URL - URL, err := url.Parse(input) - if err != nil { - return nil, ErrInputIsNotAnURL - } - if URL.Scheme != "http" && URL.Scheme != "https" { - return nil, ErrUnsupportedInput - } - // 2. Find the testhelper - testhelpers, _ := sess.GetTestHelpersByName("web-connectivity") - var testhelper *model.OOAPIService - for _, th := range testhelpers { - if th.Type == "https" { - testhelper = &th - break - } - } - if testhelper == nil { - return nil, ErrNoAvailableTestHelpers - } - testhelper.Address = "https://1.th.ooni.org/api/v1/websteps" // TODO(bassosimone): remove! - out := make(chan *model.ExperimentAsyncTestKeys) - go mx.runAsync(ctx, sess, input, testhelper, out) - return out, nil -} - -var measurerResolvers = []*measurex.ResolverInfo{{ - Network: "system", - Address: "", -}, { - Network: "udp", - Address: "8.8.4.4:53", -}} - -func (mx *Measurer) runAsync(ctx context.Context, sess model.ExperimentSession, - URL string, th *model.OOAPIService, out chan<- *model.ExperimentAsyncTestKeys) { - defer close(out) - helper := &measurerMeasureURLHelper{ - Clnt: sess.DefaultHTTPClient(), - Logger: sess.Logger(), - THURL: th.Address, - UserAgent: sess.UserAgent(), - } - mmx := &measurex.Measurer{ - Begin: time.Now(), - HTTPClient: sess.DefaultHTTPClient(), - MeasureURLHelper: helper, - Logger: sess.Logger(), - Resolvers: measurerResolvers, - TLSHandshaker: netxlite.NewTLSHandshakerStdlib(sess.Logger()), - } - cookies := measurex.NewCookieJar() - const parallelism = 3 - in := mmx.MeasureURLAndFollowRedirections( - ctx, parallelism, URL, measurex.NewHTTPRequestHeaderForMeasuring(), cookies) - for m := range in { - out <- &model.ExperimentAsyncTestKeys{ - Extensions: map[string]int64{ - archival.ExtHTTP.Name: archival.ExtHTTP.V, - archival.ExtDNS.Name: archival.ExtDNS.V, - archival.ExtNetevents.Name: archival.ExtNetevents.V, - archival.ExtTCPConnect.Name: archival.ExtTCPConnect.V, - archival.ExtTLSHandshake.Name: archival.ExtTLSHandshake.V, - }, - Input: model.MeasurementTarget(m.URL), - MeasurementRuntime: m.TotalRuntime.Seconds(), - TestKeys: &TestKeys{ - ArchivalURLMeasurement: measurex.NewArchivalURLMeasurement(m), - }, - } - } -} - -// measurerMeasureURLHelper injects the TH into the normal -// URL measurement flow implemented by measurex. -type measurerMeasureURLHelper struct { - // Clnt is the MANDATORY client to use - Clnt model.HTTPClient - - // Logger is the MANDATORY Logger to use - Logger model.Logger - - // THURL is the MANDATORY TH URL. - THURL string - - // UserAgent is the OPTIONAL user-agent to use. - UserAgent string -} - -func (mth *measurerMeasureURLHelper) LookupExtraHTTPEndpoints( - ctx context.Context, URL *url.URL, headers http.Header, - curEndpoints ...*measurex.HTTPEndpoint) ( - []*measurex.HTTPEndpoint, *measurex.THMeasurement, error) { - cc := &THClientCall{ - Endpoints: measurex.HTTPEndpointsToEndpoints(curEndpoints), - HTTPClient: mth.Clnt, - Header: headers, - THURL: mth.THURL, - TargetURL: URL.String(), - UserAgent: mth.UserAgent, - } - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - ol := measurex.NewOperationLogger( - mth.Logger, "THClientCall %s", URL.String()) - resp, err := cc.Call(ctx) - ol.Stop(err) - if err != nil { - return nil, resp, err - } - var out []*measurex.HTTPEndpoint - for _, epnt := range resp.Endpoints { - out = append(out, &measurex.HTTPEndpoint{ - Domain: URL.Hostname(), - Network: epnt.Network, - Address: epnt.Address, - SNI: URL.Hostname(), - ALPN: measurex.ALPNForHTTPEndpoint(epnt.Network), - URL: URL, - Header: headers, - }) - } - return out, resp, nil -} - -// Run implements ExperimentMeasurer.Run. -func (mx *Measurer) Run(ctx context.Context, sess model.ExperimentSession, - measurement *model.Measurement, callbacks model.ExperimentCallbacks) error { - return errors.New("sync run is not implemented") -} - -// SummaryKeys contains summary keys for this experiment. -// -// Note that this structure is part of the ABI contract with ooniprobe -// therefore we should be careful when changing it. -type SummaryKeys struct { - Accessible bool `json:"accessible"` - Blocking string `json:"blocking"` - IsAnomaly bool `json:"-"` -} - -// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys. -func (mx *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) { - sk := SummaryKeys{} - return sk, nil -} diff --git a/internal/engine/experiment/webstepsx/th.go b/internal/engine/experiment/webstepsx/th.go deleted file mode 100644 index 7b92f87..0000000 --- a/internal/engine/experiment/webstepsx/th.go +++ /dev/null @@ -1,372 +0,0 @@ -package webstepsx - -// -// TH (Test Helper) -// -// This file contains an implementation of the -// (proposed) websteps test helper spec. -// - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/measurex" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/runtimex" - "github.com/ooni/probe-cli/v3/internal/version" -) - -// -// Messages exchanged by the TH client and server -// - -// THClientRequest is the request received by the test helper. -type THClientRequest struct { - // Endpoints is a list of endpoints to measure. - Endpoints []*measurex.Endpoint - - // URL is the URL we want to measure. - URL string - - // HTTPRequestHeaders contains the request headers. - HTTPRequestHeaders http.Header -} - -// THServerResponse is the response from the test helper. -type THServerResponse = measurex.THMeasurement - -// thMaxAcceptableBodySize is the maximum acceptable body size by TH code. -const thMaxAcceptableBodySize = 1 << 20 - -// -// TH client implementation -// - -// THClient is the high-level API to invoke the TH. This API -// should be used by command line clients. -type THClient struct { - // DNSServers is the MANDATORY list of DNS-over-UDP - // servers to use to discover endpoints locally. - DNServers []*measurex.ResolverInfo - - // HTTPClient is the MANDATORY HTTP client to - // use for contacting the TH. - HTTPClient model.HTTPClient - - // ServerURL is the MANDATORY URL of the TH HTTP endpoint. - ServerURL string -} - -// Run calls the TH and returns the response or an error. -// -// Arguments: -// -// - ctx is the context with timeout/deadline/cancellation -// -// - URL is the URL the TH server should measure for us -// -// Algorithm: -// -// - use DNSServers to discover extra endpoints for the target URL -// -// - call the TH using the HTTPClient and the ServerURL -// -// - return response or error. -func (c *THClient) Run(ctx context.Context, URL string) (*THServerResponse, error) { - parsed, err := url.Parse(URL) - if err != nil { - return nil, err - } - mx := measurex.NewMeasurerWithDefaultSettings() - var dns []*measurex.DNSMeasurement - const parallelism = 3 - for m := range mx.LookupURLHostParallel(ctx, parallelism, parsed, c.DNServers...) { - dns = append(dns, m) - } - endpoints, err := measurex.AllEndpointsForURL(parsed, dns...) - if err != nil { - return nil, err - } - return (&THClientCall{ - Endpoints: endpoints, - HTTPClient: c.HTTPClient, - Header: measurex.NewHTTPRequestHeaderForMeasuring(), - THURL: c.ServerURL, - TargetURL: URL, - }).Call(ctx) -} - -// THClientCall allows to perform a single TH client call. Make sure -// you fill all the fields marked as MANDATORY before use. -type THClientCall struct { - // Endpoints contains the MANDATORY endpoints we discovered. - Endpoints []*measurex.Endpoint - - // HTTPClient is the MANDATORY HTTP client to - // use for contacting the TH. - HTTPClient model.HTTPClient - - // Header contains the MANDATORY request headers. - Header http.Header - - // THURL is the MANDATORY test helper URL. - THURL string - - // TargetURL is the MANDATORY URL to measure. - TargetURL string - - // UserAgent is the OPTIONAL user-agent to use. - UserAgent string -} - -// Call performs the specified TH call and returns either a response or an error. -func (c *THClientCall) Call(ctx context.Context) (*THServerResponse, error) { - creq := &THClientRequest{ - Endpoints: c.Endpoints, - URL: c.TargetURL, - HTTPRequestHeaders: c.Header, - } - reqBody, err := json.Marshal(creq) - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext( - ctx, "POST", c.THURL, bytes.NewReader(reqBody)) - if err != nil { - return nil, err - } - req.Header.Set("User-Agent", c.UserAgent) - return c.httpClientDo(req) -} - -// errTHRequestFailed is the error returned if the TH response is not 200 Ok. -var errTHRequestFailed = errors.New("th: request failed") - -func (c *THClientCall) httpClientDo(req *http.Request) (*THServerResponse, error) { - resp, err := c.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - if resp.StatusCode != 200 { // THHandler returns either 400 or 200 - return nil, errTHRequestFailed - } - r := io.LimitReader(resp.Body, thMaxAcceptableBodySize) - respBody, err := netxlite.ReadAllContext(req.Context(), r) - if err != nil { - return nil, err - } - var sresp THServerResponse - if err := json.Unmarshal(respBody, &sresp); err != nil { - return nil, err - } - return &sresp, nil -} - -// -// TH server implementation -// - -// THHandler implements the test helper API. -// -// This handler exposes a unique HTTP endpoint that you need to -// mount to the desired path when creating the server. -// -// The canonical mount point for the HTTP endpoint is /api/v1/websteps. -// -// Accepted methods and request body: -// -// - we only accept POST; -// -// - we expect a THClientRequest as the body. -// -// Status code and response body: -// -// - on success, status is 200 and THServerResponse is the body; -// -// - on failure, status is 400 and there is no body. -// -type THHandler struct{} - -// ServerHTTP implements http.Handler.ServeHTTP. -func (h *THHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - w.Header().Add("Server", fmt.Sprintf("oohelperd/%s", version.Version)) - if req.Method != "POST" { - w.WriteHeader(400) - return - } - reader := io.LimitReader(req.Body, thMaxAcceptableBodySize) - data, err := netxlite.ReadAllContext(req.Context(), reader) - if err != nil { - w.WriteHeader(400) - return - } - var creq THClientRequest - if err := json.Unmarshal(data, &creq); err != nil { - w.WriteHeader(400) - return - } - cresp, err := h.singleStep(req.Context(), &creq) - if err != nil { - w.WriteHeader(400) - return - } - // We assume that the following call cannot fail because it's a - // clearly serializable data structure. - data, err = json.Marshal(cresp) - runtimex.PanicOnError(err, "json.Marshal failed") - w.Header().Add("Content-Type", "application/json") - w.Write(data) -} - -// singleStep performs a singleStep measurement. -// -// The function name derives from the definition (we invented) -// of "web steps". Each redirection is a step. For each step you -// need to figure out the endpoints to use with the DNS. After -// that, you need to check all endpoints. Because here we do not -// perform redirection, this is just a single "step". -// -// The algorithm is the following: -// -// 1. parse the URL and return error if it does not parse or -// the scheme is neither HTTP nor HTTPS; -// -// 2. discover additional endpoints using a suitable DoH -// resolver and the URL's hostname as the domain; -// -// 3. measure each discovered endpoint. -// -// The return value is either a THServerResponse or an error. -func (h *THHandler) singleStep( - ctx context.Context, req *THClientRequest) (*THServerResponse, error) { - mx := measurex.NewMeasurerWithDefaultSettings() - mx.MeasureURLHelper = &thMeasureURLHelper{req.Endpoints} - mx.Resolvers = []*measurex.ResolverInfo{{ - Network: measurex.ResolverForeign, - ForeignResolver: thResolver, - }} - jar := measurex.NewCookieJar() - const parallelism = 3 - meas, err := mx.MeasureURL(ctx, parallelism, req.URL, req.HTTPRequestHeaders, jar) - if err != nil { - return nil, err - } - return &THServerResponse{ - DNS: meas.DNS, - Endpoints: h.simplifyEndpoints(meas.Endpoints), - }, nil -} - -func (h *THHandler) simplifyEndpoints( - in []*measurex.HTTPEndpointMeasurement) (out []*measurex.HTTPEndpointMeasurement) { - for _, epnt := range in { - out = append(out, &measurex.HTTPEndpointMeasurement{ - URL: epnt.URL, - Network: epnt.Network, - Address: epnt.Address, - Measurement: h.simplifyMeasurement(epnt.Measurement), - }) - } - return -} - -func (h *THHandler) simplifyMeasurement(in *measurex.Measurement) (out *measurex.Measurement) { - out = &measurex.Measurement{ - Connect: in.Connect, - TLSHandshake: h.simplifyHandshake(in.TLSHandshake), - QUICHandshake: h.simplifyHandshake(in.QUICHandshake), - LookupHost: in.LookupHost, - LookupHTTPSSvc: in.LookupHTTPSSvc, - HTTPRoundTrip: h.simplifyHTTPRoundTrip(in.HTTPRoundTrip), - } - return -} - -func (h *THHandler) simplifyHandshake( - in []*measurex.QUICTLSHandshakeEvent) (out []*measurex.QUICTLSHandshakeEvent) { - for _, ev := range in { - out = append(out, &measurex.QUICTLSHandshakeEvent{ - CipherSuite: ev.CipherSuite, - Failure: ev.Failure, - NegotiatedProto: ev.NegotiatedProto, - TLSVersion: ev.TLSVersion, - PeerCerts: nil, - Finished: 0, - RemoteAddr: ev.RemoteAddr, - SNI: ev.SNI, - ALPN: ev.ALPN, - SkipVerify: ev.SkipVerify, - Oddity: ev.Oddity, - Network: ev.Network, - Started: 0, - }) - } - return -} - -func (h *THHandler) simplifyHTTPRoundTrip( - in []*measurex.HTTPRoundTripEvent) (out []*measurex.HTTPRoundTripEvent) { - for _, ev := range in { - out = append(out, &measurex.HTTPRoundTripEvent{ - Failure: ev.Failure, - Method: ev.Method, - URL: ev.URL, - RequestHeaders: ev.RequestHeaders, - StatusCode: ev.StatusCode, - ResponseHeaders: ev.ResponseHeaders, - ResponseBody: nil, // we don't transfer the body - ResponseBodyLength: ev.ResponseBodyLength, - ResponseBodyIsTruncated: ev.ResponseBodyIsTruncated, - ResponseBodyIsUTF8: ev.ResponseBodyIsUTF8, - Finished: ev.Finished, - Started: ev.Started, - Oddity: ev.Oddity, - }) - } - return -} - -type thMeasureURLHelper struct { - epnts []*measurex.Endpoint -} - -func (thh *thMeasureURLHelper) LookupExtraHTTPEndpoints( - ctx context.Context, URL *url.URL, headers http.Header, - serverEpnts ...*measurex.HTTPEndpoint) ( - epnts []*measurex.HTTPEndpoint, thMeaurement *measurex.THMeasurement, err error) { - for _, epnt := range thh.epnts { - epnts = append(epnts, &measurex.HTTPEndpoint{ - Domain: URL.Hostname(), - Network: epnt.Network, - Address: epnt.Address, - SNI: URL.Hostname(), - ALPN: measurex.ALPNForHTTPEndpoint(epnt.Network), - URL: URL, - Header: headers, // but overriden later anyway - }) - } - return -} - -// thResolverURL is the DNS resolver URL used by the TH. We use an -// encrypted resolver to reduce the risk that there is DNS-over-UDP -// censorship in the place where we deploy the TH. -const thResolverURL = "https://dns.google/dns-query" - -// thResolver is the DNS resolver used by the TH. -// -// Here we're using github.com/apex/log as the logger, which -// is fine because this is backend only code. -var thResolver = netxlite.WrapResolver(log.Log, netxlite.NewSerialResolver( - netxlite.NewDNSOverHTTPSTransport(http.DefaultClient, thResolverURL), -)) diff --git a/internal/netxlite/filtering/tproxy.go b/internal/netxlite/filtering/tproxy.go deleted file mode 100644 index 2ac3eec..0000000 --- a/internal/netxlite/filtering/tproxy.go +++ /dev/null @@ -1,357 +0,0 @@ -package filtering - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net" - "os" - "time" - - "github.com/miekg/dns" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// TProxyPolicy is a policy for TPRoxy. -type TProxyPolicy string - -const ( - // TProxyPolicyTCPDropSYN simulates a SYN segment being dropped. - TProxyPolicyTCPDropSYN = TProxyPolicy("tcp-drop-syn") - - // TProxyPolicyTCPRejectSYN simulates a closed TCP port. - TProxyPolicyTCPRejectSYN = TProxyPolicy("tcp-reject-syn") - - // TProxyPolicyDropData drops outgoing data of an - // established TCP/UDP connection. - TProxyPolicyDropData = TProxyPolicy("drop-data") - - // TProxyPolicyHijackDNS causes the dialer to replace the target - // address with the address of the local censored resolver. - TProxyPolicyHijackDNS = TProxyPolicy("hijack-dns") - - // TProxyPolicyHijackTLS causes the dialer to replace the target - // address with the address of the local censored TLS server. - TProxyPolicyHijackTLS = TProxyPolicy("hijack-tls") - - // TProxyPolicyHijackHTTP causes the dialer to replace the target - // address with the address of the local censored HTTP server. - TProxyPolicyHijackHTTP = TProxyPolicy("hijack-http") -) - -// TProxyConfig contains configuration for TProxy. -type TProxyConfig struct { - // DNSCache is the cached used when the domains policy is "cache". Note - // that the map MUST contain FQDNs. That is, you need to append - // a final dot to the domain name (e.g., `example.com.`). If you - // use the NewTProxyConfig factory, you don't need to worry about this - // issue, because the factory will canonicalize non-canonical - // entries. Otherwise, you can explicitly call the CanonicalizeDNS - // method _before_ using the TProxy. - DNSCache map[string][]string - - // Domains contains rules for filtering the lookup of domains. Note - // that the map MUST contain FQDNs. That is, you need to append - // a final dot to the domain name (e.g., `example.com.`). If you - // use the NewTProxyConfig factory, you don't need to worry about this - // issue, because the factory will canonicalize non-canonical - // entries. Otherwise, you can explicitly call the CanonicalizeDNS - // method _before_ using the TProxy. - Domains map[string]DNSAction - - // Endpoints contains rules for filtering TCP/UDP endpoints. - Endpoints map[string]TProxyPolicy - - // SNIs contains rules for filtering TLS SNIs. - SNIs map[string]TLSAction - - // Hosts contains rules for filtering by HTTP host. - Hosts map[string]HTTPAction -} - -// NewTProxyConfig reads the TProxyConfig from the given file. -func NewTProxyConfig(file string) (*TProxyConfig, error) { - data, err := os.ReadFile(file) - if err != nil { - return nil, err - } - var config TProxyConfig - if err := json.Unmarshal(data, &config); err != nil { - return nil, err - } - config.CanonicalizeDNS() - return &config, nil -} - -// CanonicalizeDNS ensures all DNS names are canonicalized. This method -// modifies the TProxyConfig structure in place. -func (c *TProxyConfig) CanonicalizeDNS() { - domains := make(map[string]DNSAction) - for domain, policy := range c.Domains { - domains[dns.CanonicalName(domain)] = policy - } - c.Domains = domains - cache := make(map[string][]string) - for domain, addrs := range c.DNSCache { - cache[dns.CanonicalName(domain)] = addrs - } - c.DNSCache = cache -} - -// TProxy is a model.UnderlyingNetworkLibrary that implements self censorship. -type TProxy struct { - // config contains settings for TProxy. - config *TProxyConfig - - // dnsClient is the DNS client we'll internally use. - dnsClient model.Resolver - - // dnsListener is the DNS listener. - dnsListener DNSListener - - // httpListener is the HTTP listener. - httpListener net.Listener - - // listenUDP allows overriding net.ListenUDP calls in tests - listenUDP func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) - - // logger is the underlying logger to use. - logger model.InfoLogger - - // tlsListener is the TLS listener. - tlsListener net.Listener -} - -// -// Constructor and destructor -// - -// NewTProxy creates a new TProxy instance. -func NewTProxy(config *TProxyConfig, logger model.InfoLogger) (*TProxy, error) { - return newTProxy(config, logger, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1:0") -} - -func newTProxy(config *TProxyConfig, logger model.InfoLogger, dnsListenerAddr, - tlsListenerAddr, httpListenerAddr string) (*TProxy, error) { - p := &TProxy{ - config: config, - listenUDP: func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) { - return net.ListenUDP(network, laddr) - }, - logger: logger, - } - if err := p.newDNSListener(dnsListenerAddr); err != nil { - return nil, err - } - p.newDNSClient(logger) - if err := p.newTLSListener(tlsListenerAddr, logger); err != nil { - p.dnsListener.Close() - return nil, err - } - if err := p.newHTTPListener(httpListenerAddr); err != nil { - p.dnsListener.Close() - p.tlsListener.Close() - return nil, err - } - return p, nil -} - -func (p *TProxy) newDNSListener(listenAddr string) error { - var err error - dnsProxy := &DNSProxy{Cache: p.config.DNSCache, OnQuery: p.onQuery} - p.dnsListener, err = dnsProxy.Start(listenAddr) - return err -} - -func (p *TProxy) newDNSClient(logger model.DebugLogger) { - dialer := netxlite.NewDialerWithoutResolver(logger) - p.dnsClient = netxlite.NewResolverUDP(logger, dialer, p.dnsListener.LocalAddr().String()) -} - -func (p *TProxy) newTLSListener(listenAddr string, logger model.DebugLogger) error { - var err error - tlsProxy := &TLSProxy{OnIncomingSNI: p.onIncomingSNI} - p.tlsListener, err = tlsProxy.Start(listenAddr) - return err -} - -func (p *TProxy) newHTTPListener(listenAddr string) error { - var err error - httpProxy := &HTTPProxy{OnIncomingHost: p.onIncomingHost} - p.httpListener, err = httpProxy.Start(listenAddr) - return err -} - -// Close closes the resources used by a TProxy. -func (p *TProxy) Close() error { - p.dnsClient.CloseIdleConnections() - p.dnsListener.Close() - p.httpListener.Close() - p.tlsListener.Close() - return nil -} - -// -// QUIC -// - -// ListenUDP implements netxlite.TProxy.ListenUDP. -func (p *TProxy) ListenUDP(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) { - pconn, err := p.listenUDP(network, laddr) - if err != nil { - return nil, err - } - return &tProxyUDPLikeConn{UDPLikeConn: pconn, proxy: p}, nil -} - -// tProxyUDPLikeConn is a TProxy-aware UDPLikeConn. -type tProxyUDPLikeConn struct { - // UDPLikeConn is the underlying conn type. - model.UDPLikeConn - - // proxy refers to the TProxy. - proxy *TProxy -} - -// WriteTo implements UDPLikeConn.WriteTo. This function will -// apply the proper tproxy policies, if required. -func (c *tProxyUDPLikeConn) WriteTo(pkt []byte, addr net.Addr) (int, error) { - endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network()) - policy := c.proxy.config.Endpoints[endpoint] - switch policy { - case TProxyPolicyDropData: - c.proxy.logger.Infof("tproxy: WriteTo: %s => %s", endpoint, policy) - return len(pkt), nil - default: - return c.UDPLikeConn.WriteTo(pkt, addr) - } -} - -// -// System resolver -// - -// LookupHost implements netxlite.TProxy.LookupHost. -func (p *TProxy) LookupHost(ctx context.Context, domain string) ([]string, error) { - return p.dnsClient.LookupHost(ctx, domain) -} - -// -// Dialer -// - -// NewSimpleDialer implements netxlite.TProxy.NewTProxyDialer. -func (p *TProxy) NewSimpleDialer(timeout time.Duration) model.SimpleDialer { - return &tProxyDialer{ - dialer: &net.Dialer{Timeout: timeout}, - proxy: p, - } -} - -// tProxyDialer is a TProxy-aware Dialer. -type tProxyDialer struct { - // dialer is the underlying network dialer. - dialer *net.Dialer - - // proxy refers to the TProxy. - proxy *TProxy -} - -// DialContext behaves like net.Dialer.DialContext. This function will -// apply the proper tproxy policies, if required. -func (d *tProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { - endpoint := fmt.Sprintf("%s/%s", address, network) - policy := d.proxy.config.Endpoints[endpoint] - switch policy { - case TProxyPolicyTCPDropSYN: - d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) - var cancel context.CancelFunc - const timeout = 70 * time.Second - ctx, cancel = context.WithTimeout(ctx, timeout) - defer cancel() - <-ctx.Done() - return nil, errors.New("i/o timeout") - case TProxyPolicyTCPRejectSYN: - d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) - return nil, netxlite.ECONNREFUSED - case TProxyPolicyHijackDNS: - d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) - address = d.proxy.dnsListener.LocalAddr().String() - case TProxyPolicyHijackTLS: - d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) - address = d.proxy.tlsListener.Addr().String() - case TProxyPolicyHijackHTTP: - d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) - address = d.proxy.httpListener.Addr().String() - default: - // nothing - } - conn, err := d.dialer.DialContext(ctx, network, address) - if err != nil { - return nil, err - } - return &tProxyConn{Conn: conn, proxy: d.proxy}, nil -} - -// tProxyConn is a TProxy-aware net.Conn. -type tProxyConn struct { - // Conn is the underlying conn. - net.Conn - - // proxy refers to the TProxy. - proxy *TProxy -} - -// Write implements Conn.Write. This function will apply -// the proper tproxy policies, if required. -func (c *tProxyConn) Write(b []byte) (int, error) { - addr := c.Conn.RemoteAddr() - endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network()) - policy := c.proxy.config.Endpoints[endpoint] - switch policy { - case TProxyPolicyDropData: - c.proxy.logger.Infof("tproxy: Write: %s => %s", endpoint, policy) - return len(b), nil - default: - return c.Conn.Write(b) - } -} - -// -// Filtering policies implementation -// - -// onQuery is called for filtering outgoing DNS queries. -func (p *TProxy) onQuery(domain string) DNSAction { - policy := p.config.Domains[domain] - if policy == "" { - policy = DNSActionPass - } else { - p.logger.Infof("tproxy: DNS: %s => %s", domain, policy) - } - return policy -} - -// onIncomingSNI is called for filtering SNI values. -func (p *TProxy) onIncomingSNI(sni string) TLSAction { - policy := p.config.SNIs[sni] - if policy == "" { - policy = TLSActionPass - } else { - p.logger.Infof("tproxy: TLS: %s => %s", sni, policy) - } - return policy -} - -// onIncomingHost is called for filtering HTTP hosts. -func (p *TProxy) onIncomingHost(host string) HTTPAction { - policy := p.config.Hosts[host] - if policy == "" { - policy = HTTPActionPass - } else { - p.logger.Infof("tproxy: HTTP: %s => %s", host, policy) - } - return policy -} diff --git a/internal/netxlite/filtering/tproxy_test.go b/internal/netxlite/filtering/tproxy_test.go deleted file mode 100644 index c646087..0000000 --- a/internal/netxlite/filtering/tproxy_test.go +++ /dev/null @@ -1,578 +0,0 @@ -package filtering - -import ( - "context" - "crypto/tls" - "errors" - "net" - "net/http" - "path/filepath" - "strings" - "syscall" - "testing" - "time" - - "github.com/apex/log" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/model/mocks" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// tProxyDialerAdapter adapts a netxlite.TProxyDialer to be a netxlite.Dialer. -type tProxyDialerAdapter struct { - model.SimpleDialer -} - -// CloseIdleConnections implements Dialer.CloseIdleConnections. -func (*tProxyDialerAdapter) CloseIdleConnections() { - // nothing -} - -func TestNewTProxyConfig(t *testing.T) { - t.Run("with nonexistent file", func(t *testing.T) { - config, err := NewTProxyConfig(filepath.Join("testdata", "nonexistent")) - if !errors.Is(err, syscall.ENOENT) { - t.Fatal("unexpected err", err) - } - if config != nil { - t.Fatal("expected nil config here") - } - }) - - t.Run("with file containing invalid JSON", func(t *testing.T) { - config, err := NewTProxyConfig(filepath.Join("testdata", "invalid.json")) - if err == nil || !strings.HasSuffix(err.Error(), "unexpected end of JSON input") { - t.Fatal("unexpected err", err) - } - if config != nil { - t.Fatal("expected nil config here") - } - }) - - t.Run("with file containing valid JSON", func(t *testing.T) { - config, err := NewTProxyConfig(filepath.Join("testdata", "valid.json")) - if err != nil { - t.Fatal(err) - } - if config == nil { - t.Fatal("expected non-nil config here") - } - if config.Domains["x.org."] != "pass" { - t.Fatal("did not auto-canonicalize config.Domains") - } - if len(config.DNSCache["dns.google."]) != 2 { - t.Fatal("did not auto-canonicalize config.DNSCache") - } - }) -} - -func TestNewTProxy(t *testing.T) { - t.Run("successful creation and destruction", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - if err := proxy.Close(); err != nil { - t.Fatal(err) - } - }) - - t.Run("cannot create DNS listener", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := newTProxy(config, log.Log, "127.0.0.1", "", "") - if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { - t.Fatal("unexpected err", err) - } - if proxy != nil { - t.Fatal("expected nil proxy here") - } - }) - - t.Run("cannot create TLS listener", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1", "") - if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { - t.Fatal("unexpected err", err) - } - if proxy != nil { - t.Fatal("expected nil proxy here") - } - }) - - t.Run("cannot create HTTP listener", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1") - if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { - t.Fatal("unexpected err", err) - } - if proxy != nil { - t.Fatal("expected nil proxy here") - } - }) -} - -func TestTProxyQUIC(t *testing.T) { - t.Run("ListenUDP", func(t *testing.T) { - t.Run("failure", func(t *testing.T) { - proxy, err := NewTProxy(&TProxyConfig{}, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - pconn, err := proxy.ListenUDP("tcp", &net.UDPAddr{}) - if err == nil || !strings.HasSuffix(err.Error(), "unknown network tcp") { - t.Fatal("unexpected err", err) - } - if pconn != nil { - t.Fatal("expected nil pconn here") - } - }) - - t.Run("success", func(t *testing.T) { - proxy, err := NewTProxy(&TProxyConfig{}, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) - if err != nil { - t.Fatal(err) - } - uconn := pconn.(*tProxyUDPLikeConn) - if uconn.proxy != proxy { - t.Fatal("proxy not correctly set") - } - if _, okay := uconn.UDPLikeConn.(*net.UDPConn); !okay { - t.Fatal("underlying connection should be an UDPConn") - } - uconn.Close() - }) - }) - - t.Run("WriteTo", func(t *testing.T) { - t.Run("without the drop policy", func(t *testing.T) { - proxy, err := NewTProxy(&TProxyConfig{}, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - var called bool - proxy.listenUDP = func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) { - return &mocks.UDPLikeConn{ - MockWriteTo: func(p []byte, addr net.Addr) (int, error) { - called = true - return len(p), nil - }, - }, nil - } - pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 128) - count, err := pconn.WriteTo(data, &net.UDPAddr{}) - if err != nil { - t.Fatal(err) - } - if count != len(data) { - t.Fatal("unexpected number of bytes written") - } - if !called { - t.Fatal("not called") - } - }) - - t.Run("with the drop policy", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "127.0.0.1:1234/udp": TProxyPolicyDropData, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - var called bool - proxy.listenUDP = func(network string, laddr *net.UDPAddr) (model.UDPLikeConn, error) { - return &mocks.UDPLikeConn{ - MockWriteTo: func(p []byte, addr net.Addr) (int, error) { - called = true - return len(p), nil - }, - }, nil - } - pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) - if err != nil { - t.Fatal(err) - } - data := make([]byte, 128) - destAddr := &net.UDPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 1234, - Zone: "", - } - count, err := pconn.WriteTo(data, destAddr) - if err != nil { - t.Fatal(err) - } - if count != len(data) { - t.Fatal("unexpected number of bytes written") - } - if called { - t.Fatal("called") - } - }) - }) -} - -func TestTProxyLookupHost(t *testing.T) { - t.Run("without filtering", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - ctx := context.Background() - addrs, err := proxy.LookupHost(ctx, "dns.google") - if err != nil { - t.Fatal(err) - } - if len(addrs) < 2 { - t.Fatal("too few addrs") - } - }) - - t.Run("with filtering", func(t *testing.T) { - config := &TProxyConfig{ - Domains: map[string]DNSAction{ - "dns.google.": DNSActionNXDOMAIN, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - ctx := context.Background() - addrs, err := proxy.LookupHost(ctx, "dns.google") - if err == nil || err.Error() != "dns_nxdomain_error" { - t.Fatal("unexpected err", err) - } - if len(addrs) != 0 { - t.Fatal("too many addrs") - } - }) -} - -func TestTProxyOnIncomingSNI(t *testing.T) { - t.Run("without filtering", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "8.8.8.8:443/tcp": TProxyPolicyHijackTLS, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - ctx := context.Background() - dialer := proxy.NewSimpleDialer(10 * time.Second) - conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443") - if err != nil { - t.Fatal(err) - } - tconn := tls.Client(conn, &tls.Config{ServerName: "dns.google"}) - err = tconn.HandshakeContext(ctx) - if err != nil { - t.Fatal(err) - } - tconn.Close() - }) - - t.Run("with filtering", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "8.8.8.8:443/tcp": TProxyPolicyHijackTLS, - }, - SNIs: map[string]TLSAction{ - "dns.google": TLSActionReset, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - ctx := context.Background() - dialer := proxy.NewSimpleDialer(10 * time.Second) - conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443") - if err != nil { - t.Fatal(err) - } - tlsh := netxlite.NewTLSHandshakerStdlib(log.Log) - tconn, _, err := tlsh.Handshake(ctx, conn, &tls.Config{ServerName: "dns.google"}) - if err == nil || err.Error() != netxlite.FailureConnectionReset { - t.Fatal("unexpected err", err) - } - if tconn != nil { - t.Fatal("expected nil tconn") - } - conn.Close() - }) -} - -func TestTProxyOnIncomingHost(t *testing.T) { - t.Run("without filtering", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "130.192.16.171:80/tcp": TProxyPolicyHijackHTTP, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := proxy.NewSimpleDialer(10 * time.Second) - req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "nexa.polito.it" - txp := &http.Transport{DialContext: dialer.DialContext} - resp, err := txp.RoundTrip(req) - if err != nil { - t.Fatal(err) - } - resp.Body.Close() - }) - - t.Run("with filtering", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "130.192.16.171:80/tcp": TProxyPolicyHijackHTTP, - }, - Hosts: map[string]HTTPAction{ - "nexa.polito.it": HTTPActionReset, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := netxlite.WrapDialer( - log.Log, - netxlite.NewResolverStdlib(log.Log), - &tProxyDialerAdapter{ - proxy.NewSimpleDialer(10 * time.Second), - }, - ) - req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "nexa.polito.it" - txp := &http.Transport{DialContext: dialer.DialContext} - resp, err := txp.RoundTrip(req) - if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionReset) { - t.Fatal("unexpected err", err) - } - if resp != nil { - t.Fatal("expected nil resp here") - } - }) -} - -func TestTProxyDial(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - t.Run("with drop SYN", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "130.192.16.171:80/tcp": TProxyPolicyTCPDropSYN, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := proxy.NewSimpleDialer(10 * time.Second) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - req, err := http.NewRequestWithContext(ctx, "GET", "http://130.192.16.171:80", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "nexa.polito.it" - txp := &http.Transport{DialContext: dialer.DialContext} - resp, err := txp.RoundTrip(req) - if !errors.Is(err, context.DeadlineExceeded) { - t.Fatal("unexpected err", err) - } - if resp != nil { - t.Fatal("expected nil resp here") - } - }) - - t.Run("with reject SYN", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "130.192.16.171:80/tcp": TProxyPolicyTCPRejectSYN, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := netxlite.WrapDialer(log.Log, - netxlite.NewResolverStdlib(log.Log), - &tProxyDialerAdapter{ - proxy.NewSimpleDialer(10 * time.Second)}) - req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "nexa.polito.it" - txp := &http.Transport{DialContext: dialer.DialContext} - resp, err := txp.RoundTrip(req) - if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionRefused) { - t.Fatal("unexpected err", err) - } - if resp != nil { - t.Fatal("expected nil resp here") - } - }) - - t.Run("with drop data", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "130.192.16.171:80/tcp": TProxyPolicyDropData, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := proxy.NewSimpleDialer(10 * time.Second) - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - req, err := http.NewRequestWithContext( - ctx, "GET", "http://130.192.16.171:80", nil) - if err != nil { - t.Fatal(err) - } - req.Host = "nexa.polito.it" - txp := &http.Transport{DialContext: dialer.DialContext} - resp, err := txp.RoundTrip(req) - if !errors.Is(err, context.DeadlineExceeded) { - t.Fatal("unexpected err", err) - } - if resp != nil { - t.Fatal("expected nil resp here") - } - }) - - t.Run("with hijack DNS", func(t *testing.T) { - config := &TProxyConfig{ - Endpoints: map[string]TProxyPolicy{ - "8.8.8.8:53/udp": TProxyPolicyHijackDNS, - }, - Domains: map[string]DNSAction{ - "example.com.": DNSActionNXDOMAIN, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := proxy.NewSimpleDialer(10 * time.Second) - resolver := netxlite.NewResolverUDP( - log.Log, &tProxyDialerAdapter{dialer}, "8.8.8.8:53") - addrs, err := resolver.LookupHost(context.Background(), "example.com") - if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError { - t.Fatal("unexpected err", err) - } - if len(addrs) != 0 { - t.Fatal("expected no addrs here") - } - }) - - t.Run("with invalid destination address", func(t *testing.T) { - config := &TProxyConfig{} - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - defer proxy.Close() - dialer := proxy.NewSimpleDialer(10 * time.Second) - ctx := context.Background() - conn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1") - if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { - t.Fatal("unexpected err", err) - } - if conn != nil { - t.Fatal("expected nil conn here") - } - }) -} - -func TestTProxyDNSCache(t *testing.T) { - t.Run("without cache but with the cache rule", func(t *testing.T) { - config := &TProxyConfig{ - Domains: map[string]DNSAction{ - "dns.google.": DNSActionCache, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - ctx := context.Background() - addrs, err := proxy.LookupHost(ctx, "dns.google") - if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError { - t.Fatal("unexpected err", err) - } - if addrs != nil { - t.Fatal("expected nil addrs") - } - }) - - t.Run("with cache", func(t *testing.T) { - config := &TProxyConfig{ - DNSCache: map[string][]string{ - "dns.google.": {"8.8.8.8", "8.8.4.4"}, - }, - Domains: map[string]DNSAction{ - "dns.google.": DNSActionCache, - }, - } - proxy, err := NewTProxy(config, log.Log) - if err != nil { - t.Fatal(err) - } - ctx := context.Background() - addrs, err := proxy.LookupHost(ctx, "dns.google") - if err != nil { - t.Fatal(err) - } - if len(addrs) != 2 { - t.Fatal("expected two addrs") - } - if addrs[0] != "8.8.8.8" { - t.Fatal("invalid first address") - } - if addrs[1] != "8.8.4.4" { - t.Fatal("invalid second address") - } - }) -} diff --git a/internal/ooapi/apimodel/checkin.go b/internal/ooapi/apimodel/checkin.go deleted file mode 100644 index d6cd16e..0000000 --- a/internal/ooapi/apimodel/checkin.go +++ /dev/null @@ -1,49 +0,0 @@ -package apimodel - -import "github.com/ooni/probe-cli/v3/internal/model" - -// CheckInRequestWebConnectivity contains WebConnectivity -// specific parameters to include into CheckInRequest -type CheckInRequestWebConnectivity struct { - CategoryCodes []string `json:"category_codes"` -} - -// CheckInRequest is the check-in API request -type CheckInRequest struct { - Charging bool `json:"charging"` - OnWiFi bool `json:"on_wifi"` - Platform string `json:"platform"` - ProbeASN string `json:"probe_asn"` - ProbeCC string `json:"probe_cc"` - RunType model.RunType `json:"run_type"` - SoftwareName string `json:"software_name"` - SoftwareVersion string `json:"software_version"` - WebConnectivity CheckInRequestWebConnectivity `json:"web_connectivity"` -} - -// CheckInResponseURLInfo contains information about an URL. -type CheckInResponseURLInfo struct { - CategoryCode string `json:"category_code"` - CountryCode string `json:"country_code"` - URL string `json:"url"` -} - -// CheckInResponseWebConnectivity contains WebConnectivity -// specific information of a CheckInResponse -type CheckInResponseWebConnectivity struct { - ReportID string `json:"report_id"` - URLs []CheckInResponseURLInfo `json:"urls"` -} - -// CheckInResponse is the check-in API response -type CheckInResponse struct { - ProbeASN string `json:"probe_asn"` - ProbeCC string `json:"probe_cc"` - Tests CheckInResponseTests `json:"tests"` - V int64 `json:"v"` -} - -// CheckInResponseTests contains configuration for tests -type CheckInResponseTests struct { - WebConnectivity CheckInResponseWebConnectivity `json:"web_connectivity"` -} diff --git a/internal/ooapi/apimodel/checkreportid.go b/internal/ooapi/apimodel/checkreportid.go deleted file mode 100644 index 068a047..0000000 --- a/internal/ooapi/apimodel/checkreportid.go +++ /dev/null @@ -1,13 +0,0 @@ -package apimodel - -// CheckReportIDRequest is the CheckReportID request. -type CheckReportIDRequest struct { - ReportID string `query:"report_id" required:"true"` -} - -// CheckReportIDResponse is the CheckReportID response. -type CheckReportIDResponse struct { - Error string `json:"error"` - Found bool `json:"found"` - V int64 `json:"v"` -} diff --git a/internal/ooapi/apimodel/doc.go b/internal/ooapi/apimodel/doc.go deleted file mode 100644 index da78f36..0000000 --- a/internal/ooapi/apimodel/doc.go +++ /dev/null @@ -1,22 +0,0 @@ -// Package apimodel describes the data types used by OONI's API. -// -// If you edit this package to integrate the data model, remember to -// run `go generate ./...`. -// -// We annotate fields with tagging. When a field should be sent -// over as JSON, use the usual `json` tag. -// -// When a field needs to be sent using the query string, use -// the `query` tag instead. We limit what can be sent using the -// query string to int64, string, and bool. -// -// The `path` tag indicates that the URL path contains a -// template. We will replace the value of this field with -// the template. Note that the template should use the -// Go name of the field (e.g. `{{ .ReportID }}`) as opposed -// to the name in the tag, which is only used when we -// generate the API Swagger. -// -// The `required` tag indicates required fields. A required -// field cannot be empty (for the Go definition of empty). -package apimodel diff --git a/internal/ooapi/apimodel/login.go b/internal/ooapi/apimodel/login.go deleted file mode 100644 index 408b347..0000000 --- a/internal/ooapi/apimodel/login.go +++ /dev/null @@ -1,15 +0,0 @@ -package apimodel - -import "time" - -// LoginRequest is the login API request -type LoginRequest struct { - ClientID string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse is the login API response -type LoginResponse struct { - Expire time.Time `json:"expire"` - Token string `json:"token"` -} diff --git a/internal/ooapi/apimodel/measurementmeta.go b/internal/ooapi/apimodel/measurementmeta.go deleted file mode 100644 index e97da69..0000000 --- a/internal/ooapi/apimodel/measurementmeta.go +++ /dev/null @@ -1,25 +0,0 @@ -package apimodel - -// MeasurementMetaRequest is the MeasurementMeta Request. -type MeasurementMetaRequest struct { - ReportID string `query:"report_id" required:"true"` - Full bool `query:"full"` - Input string `query:"input"` -} - -// MeasurementMetaResponse is the MeasurementMeta Response. -type MeasurementMetaResponse struct { - Anomaly bool `json:"anomaly"` - CategoryCode string `json:"category_code"` - Confirmed bool `json:"confirmed"` - Failure bool `json:"failure"` - Input string `json:"input"` - MeasurementStartTime string `json:"measurement_start_time"` - ProbeASN int64 `json:"probe_asn"` - ProbeCC string `json:"probe_cc"` - RawMeasurement string `json:"raw_measurement"` - ReportID string `json:"report_id"` - Scores string `json:"scores"` - TestName string `json:"test_name"` - TestStartTime string `json:"test_start_time"` -} diff --git a/internal/ooapi/apimodel/openreport.go b/internal/ooapi/apimodel/openreport.go deleted file mode 100644 index 4432bba..0000000 --- a/internal/ooapi/apimodel/openreport.go +++ /dev/null @@ -1,21 +0,0 @@ -package apimodel - -// OpenReportRequest is the OpenReport request. -type OpenReportRequest struct { - DataFormatVersion string `json:"data_format_version"` - Format string `json:"format"` - ProbeASN string `json:"probe_asn"` - ProbeCC string `json:"probe_cc"` - SoftwareName string `json:"software_name"` - SoftwareVersion string `json:"software_version"` - TestName string `json:"test_name"` - TestStartTime string `json:"test_start_time"` - TestVersion string `json:"test_version"` -} - -// OpenReportResponse is the OpenReport response. -type OpenReportResponse struct { - BackendVersion string `json:"backend_version"` - ReportID string `json:"report_id"` - SupportedFormats []string `json:"supported_formats"` -} diff --git a/internal/ooapi/apimodel/psiphonconfig.go b/internal/ooapi/apimodel/psiphonconfig.go deleted file mode 100644 index 40f9726..0000000 --- a/internal/ooapi/apimodel/psiphonconfig.go +++ /dev/null @@ -1,7 +0,0 @@ -package apimodel - -// PsiphonConfigRequest is the request for the PsiphonConfig API -type PsiphonConfigRequest struct{} - -// PsiphonConfigResponse is the response from the PsiphonConfig API -type PsiphonConfigResponse map[string]interface{} diff --git a/internal/ooapi/apimodel/register.go b/internal/ooapi/apimodel/register.go deleted file mode 100644 index e167306..0000000 --- a/internal/ooapi/apimodel/register.go +++ /dev/null @@ -1,26 +0,0 @@ -package apimodel - -// RegisterRequest is the request for the Register API. -type RegisterRequest struct { - // just password - Password string `json:"password"` - - // metadata - AvailableBandwidth string `json:"available_bandwidth,omitempty"` - DeviceToken string `json:"device_token,omitempty"` - Language string `json:"language,omitempty"` - NetworkType string `json:"network_type,omitempty"` - Platform string `json:"platform"` - ProbeASN string `json:"probe_asn"` - ProbeCC string `json:"probe_cc"` - ProbeFamily string `json:"probe_family,omitempty"` - ProbeTimezone string `json:"probe_timezone,omitempty"` - SoftwareName string `json:"software_name"` - SoftwareVersion string `json:"software_version"` - SupportedTests []string `json:"supported_tests"` -} - -// RegisterResponse is the response from the Register API. -type RegisterResponse struct { - ClientID string `json:"client_id"` -} diff --git a/internal/ooapi/apimodel/submitmeasurement.go b/internal/ooapi/apimodel/submitmeasurement.go deleted file mode 100644 index da542d5..0000000 --- a/internal/ooapi/apimodel/submitmeasurement.go +++ /dev/null @@ -1,13 +0,0 @@ -package apimodel - -// SubmitMeasurementRequest is the SubmitMeasurement request. -type SubmitMeasurementRequest struct { - ReportID string `path:"report_id"` - Format string `json:"format"` - Content interface{} `json:"content"` -} - -// SubmitMeasurementResponse is the SubmitMeasurement response. -type SubmitMeasurementResponse struct { - MeasurementUID string `json:"measurement_uid"` -} diff --git a/internal/ooapi/apimodel/testhelpers.go b/internal/ooapi/apimodel/testhelpers.go deleted file mode 100644 index 775a40d..0000000 --- a/internal/ooapi/apimodel/testhelpers.go +++ /dev/null @@ -1,15 +0,0 @@ -package apimodel - -// TestHelpersRequest is the TestHelpers request. -type TestHelpersRequest struct{} - -// TestHelpersResponse is the TestHelpers response. -type TestHelpersResponse map[string][]TestHelpersHelperInfo - -// TestHelpersHelperInfo is a single helper within the -// response returned by the TestHelpers API. -type TestHelpersHelperInfo struct { - Address string `json:"address"` - Type string `json:"type"` - Front string `json:"front,omitempty"` -} diff --git a/internal/ooapi/apimodel/tortargets.go b/internal/ooapi/apimodel/tortargets.go deleted file mode 100644 index a4d7c74..0000000 --- a/internal/ooapi/apimodel/tortargets.go +++ /dev/null @@ -1,16 +0,0 @@ -package apimodel - -// TorTargetsRequest is a request for the TorTargets API. -type TorTargetsRequest struct{} - -// TorTargetsResponse is the response from the TorTargets API. -type TorTargetsResponse map[string]TorTargetsTarget - -// TorTargetsTarget is a target for the tor experiment. -type TorTargetsTarget struct { - Address string `json:"address"` - Name string `json:"name"` - Params map[string][]string `json:"params"` - Protocol string `json:"protocol"` - Source string `json:"source"` -} diff --git a/internal/ooapi/apimodel/urls.go b/internal/ooapi/apimodel/urls.go deleted file mode 100644 index dd9094f..0000000 --- a/internal/ooapi/apimodel/urls.go +++ /dev/null @@ -1,26 +0,0 @@ -package apimodel - -// URLsRequest is the URLs request. -type URLsRequest struct { - CategoryCodes string `query:"category_codes"` - CountryCode string `query:"country_code"` - Limit int64 `query:"limit"` -} - -// URLsResponse is the URLs response. -type URLsResponse struct { - Metadata URLsMetadata `json:"metadata"` - Results []URLsResponseURL `json:"results"` -} - -// URLsMetadata contains metadata in the URLs response. -type URLsMetadata struct { - Count int64 `json:"count"` -} - -// URLsResponseURL is a single URL in the URLs response. -type URLsResponseURL struct { - CategoryCode string `json:"category_code"` - CountryCode string `json:"country_code"` - URL string `json:"url"` -} diff --git a/internal/ooapi/apis.go b/internal/ooapi/apis.go deleted file mode 100644 index 9cb0c22..0000000 --- a/internal/ooapi/apis.go +++ /dev/null @@ -1,618 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:46.371368 +0200 CEST m=+0.001320792 - -package ooapi - -//go:generate go run ./internal/generator -file apis.go - -import ( - "context" - "net/http" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -// simpleCheckReportIDAPI implements the CheckReportID API. -type simpleCheckReportIDAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleCheckReportIDAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleCheckReportIDAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleCheckReportIDAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleCheckReportIDAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the CheckReportID API. -func (api *simpleCheckReportIDAPI) Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleCheckInAPI implements the CheckIn API. -type simpleCheckInAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleCheckInAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleCheckInAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleCheckInAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleCheckInAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the CheckIn API. -func (api *simpleCheckInAPI) Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleLoginAPI implements the Login API. -type simpleLoginAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleLoginAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleLoginAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleLoginAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleLoginAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the Login API. -func (api *simpleLoginAPI) Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleMeasurementMetaAPI implements the MeasurementMeta API. -type simpleMeasurementMetaAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleMeasurementMetaAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleMeasurementMetaAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleMeasurementMetaAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleMeasurementMetaAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the MeasurementMeta API. -func (api *simpleMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleRegisterAPI implements the Register API. -type simpleRegisterAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleRegisterAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleRegisterAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleRegisterAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleRegisterAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the Register API. -func (api *simpleRegisterAPI) Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleTestHelpersAPI implements the TestHelpers API. -type simpleTestHelpersAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleTestHelpersAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleTestHelpersAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleTestHelpersAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleTestHelpersAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the TestHelpers API. -func (api *simpleTestHelpersAPI) Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simplePsiphonConfigAPI implements the PsiphonConfig API. -type simplePsiphonConfigAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - Token string // mandatory - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -// WithToken returns a copy of the API where the -// value of the Token field is replaced with token. -func (api *simplePsiphonConfigAPI) WithToken(token string) callerForPsiphonConfigAPI { - out := &simplePsiphonConfigAPI{} - out.BaseURL = api.BaseURL - out.HTTPClient = api.HTTPClient - out.JSONCodec = api.JSONCodec - out.RequestMaker = api.RequestMaker - out.UserAgent = api.UserAgent - out.Token = token - return out -} - -func (api *simplePsiphonConfigAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simplePsiphonConfigAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simplePsiphonConfigAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simplePsiphonConfigAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the PsiphonConfig API. -func (api *simplePsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.Token == "" { - return nil, ErrMissingToken - } - httpReq.Header.Add("Authorization", newAuthorizationHeader(api.Token)) - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleTorTargetsAPI implements the TorTargets API. -type simpleTorTargetsAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - Token string // mandatory - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -// WithToken returns a copy of the API where the -// value of the Token field is replaced with token. -func (api *simpleTorTargetsAPI) WithToken(token string) callerForTorTargetsAPI { - out := &simpleTorTargetsAPI{} - out.BaseURL = api.BaseURL - out.HTTPClient = api.HTTPClient - out.JSONCodec = api.JSONCodec - out.RequestMaker = api.RequestMaker - out.UserAgent = api.UserAgent - out.Token = token - return out -} - -func (api *simpleTorTargetsAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleTorTargetsAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleTorTargetsAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleTorTargetsAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the TorTargets API. -func (api *simpleTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.Token == "" { - return nil, ErrMissingToken - } - httpReq.Header.Add("Authorization", newAuthorizationHeader(api.Token)) - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleURLsAPI implements the URLs API. -type simpleURLsAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleURLsAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleURLsAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleURLsAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleURLsAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the URLs API. -func (api *simpleURLsAPI) Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleOpenReportAPI implements the OpenReport API. -type simpleOpenReportAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - UserAgent string // optional -} - -func (api *simpleOpenReportAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleOpenReportAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleOpenReportAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleOpenReportAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the OpenReport API. -func (api *simpleOpenReportAPI) Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} - -// simpleSubmitMeasurementAPI implements the SubmitMeasurement API. -type simpleSubmitMeasurementAPI struct { - BaseURL string // optional - HTTPClient HTTPClient // optional - JSONCodec JSONCodec // optional - RequestMaker RequestMaker // optional - TemplateExecutor templateExecutor // optional - UserAgent string // optional -} - -func (api *simpleSubmitMeasurementAPI) baseURL() string { - if api.BaseURL != "" { - return api.BaseURL - } - return "https://ps1.ooni.io" -} - -func (api *simpleSubmitMeasurementAPI) requestMaker() RequestMaker { - if api.RequestMaker != nil { - return api.RequestMaker - } - return &defaultRequestMaker{} -} - -func (api *simpleSubmitMeasurementAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *simpleSubmitMeasurementAPI) templateExecutor() templateExecutor { - if api.TemplateExecutor != nil { - return api.TemplateExecutor - } - return &defaultTemplateExecutor{} -} - -func (api *simpleSubmitMeasurementAPI) httpClient() HTTPClient { - if api.HTTPClient != nil { - return api.HTTPClient - } - return http.DefaultClient -} - -// Call calls the SubmitMeasurement API. -func (api *simpleSubmitMeasurementAPI) Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error) { - httpReq, err := api.newRequest(ctx, req) - if err != nil { - return nil, err - } - httpReq.Header.Add("Accept", "application/json") - if api.UserAgent != "" { - httpReq.Header.Add("User-Agent", api.UserAgent) - } - httpResp, err := api.httpClient().Do(httpReq) - return api.newResponse(ctx, httpResp, err) -} diff --git a/internal/ooapi/apis_test.go b/internal/ooapi/apis_test.go deleted file mode 100644 index 7010f51..0000000 --- a/internal/ooapi/apis_test.go +++ /dev/null @@ -1,2776 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:46.817279 +0200 CEST m=+0.001015835 - -package ooapi - -//go:generate go run ./internal/generator -file apis_test.go - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "sync" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func TestCheckReportIDInvalidURL(t *testing.T) { - api := &simpleCheckReportIDAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleCheckReportIDAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckReportIDWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleCheckReportID struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.CheckReportIDResponse - url *url.URL - userAgent string -} - -func (h *handleCheckReportID) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.CheckReportIDResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestCheckReportIDRoundTrip(t *testing.T) { - // setup - handler := &handleCheckReportID{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleCheckReportIDAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestCheckReportIDMandatoryFields(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 500, - }} - api := &simpleCheckReportIDAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckReportIDRequest{} // deliberately empty - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrEmptyField) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInInvalidURL(t *testing.T) { - api := &simpleCheckInAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleCheckInAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInMarshalErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleCheckInAPI{ - JSONCodec: &FakeCodec{EncodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleCheckInAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleCheckInAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleCheckInAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleCheckInAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestCheckInWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleCheckInAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleCheckIn struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.CheckInResponse - url *url.URL - userAgent string -} - -func (h *handleCheckIn) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.CheckInResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestCheckInRoundTrip(t *testing.T) { - // setup - handler := &handleCheckIn{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleCheckInAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.CheckInRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -func TestLoginInvalidURL(t *testing.T) { - api := &simpleLoginAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleLoginAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginMarshalErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleLoginAPI{ - JSONCodec: &FakeCodec{EncodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleLoginAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleLoginAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleLoginAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleLoginAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestLoginWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleLoginAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleLogin struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.LoginResponse - url *url.URL - userAgent string -} - -func (h *handleLogin) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.LoginResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestLoginRoundTrip(t *testing.T) { - // setup - handler := &handleLogin{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.LoginRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleLoginAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.LoginRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -func TestMeasurementMetaInvalidURL(t *testing.T) { - api := &simpleMeasurementMetaAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleMeasurementMetaAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestMeasurementMetaWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleMeasurementMeta struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.MeasurementMetaResponse - url *url.URL - userAgent string -} - -func (h *handleMeasurementMeta) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.MeasurementMetaResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestMeasurementMetaRoundTrip(t *testing.T) { - // setup - handler := &handleMeasurementMeta{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleMeasurementMetaAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestMeasurementMetaMandatoryFields(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 500, - }} - api := &simpleMeasurementMetaAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.MeasurementMetaRequest{} // deliberately empty - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrEmptyField) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterInvalidURL(t *testing.T) { - api := &simpleRegisterAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleRegisterAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterMarshalErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleRegisterAPI{ - JSONCodec: &FakeCodec{EncodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleRegisterAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleRegisterAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleRegisterAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleRegisterAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestRegisterWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleRegisterAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleRegister struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.RegisterResponse - url *url.URL - userAgent string -} - -func (h *handleRegister) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.RegisterResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestRegisterRoundTrip(t *testing.T) { - // setup - handler := &handleRegister{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.RegisterRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleRegisterAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.RegisterRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -func TestTestHelpersInvalidURL(t *testing.T) { - api := &simpleTestHelpersAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleTestHelpersAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTestHelpersWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleTestHelpers struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.TestHelpersResponse - url *url.URL - userAgent string -} - -func (h *handleTestHelpers) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.TestHelpersResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestTestHelpersRoundTrip(t *testing.T) { - // setup - handler := &handleTestHelpers{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleTestHelpersAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestTestHelpersResponseLiteralNull(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`null`)}, - }} - api := &simpleTestHelpersAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrJSONLiteralNull) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigInvalidURL(t *testing.T) { - api := &simplePsiphonConfigAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWithMissingToken(t *testing.T) { - api := &simplePsiphonConfigAPI{} // no token - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrMissingToken) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simplePsiphonConfigAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestPsiphonConfigWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handlePsiphonConfig struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.PsiphonConfigResponse - url *url.URL - userAgent string -} - -func (h *handlePsiphonConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.PsiphonConfigResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestPsiphonConfigRoundTrip(t *testing.T) { - // setup - handler := &handlePsiphonConfig{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simplePsiphonConfigAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - ff.Fill(&api.Token) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestPsiphonConfigResponseLiteralNull(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`null`)}, - }} - api := &simplePsiphonConfigAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrJSONLiteralNull) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsInvalidURL(t *testing.T) { - api := &simpleTorTargetsAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWithMissingToken(t *testing.T) { - api := &simpleTorTargetsAPI{} // no token - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrMissingToken) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleTorTargetsAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestTorTargetsWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleTorTargets struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.TorTargetsResponse - url *url.URL - userAgent string -} - -func (h *handleTorTargets) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.TorTargetsResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestTorTargetsRoundTrip(t *testing.T) { - // setup - handler := &handleTorTargets{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleTorTargetsAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - ff.Fill(&api.Token) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestTorTargetsResponseLiteralNull(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`null`)}, - }} - api := &simpleTorTargetsAPI{ - HTTPClient: clnt, - Token: "fakeToken", - } - ctx := context.Background() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrJSONLiteralNull) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsInvalidURL(t *testing.T) { - api := &simpleURLsAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleURLsAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleURLsAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleURLsAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleURLsAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleURLsAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestURLsWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleURLsAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleURLs struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.URLsResponse - url *url.URL - userAgent string -} - -func (h *handleURLs) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.URLsResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestURLsRoundTrip(t *testing.T) { - // setup - handler := &handleURLs{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleURLsAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -func TestOpenReportInvalidURL(t *testing.T) { - api := &simpleOpenReportAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleOpenReportAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportMarshalErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleOpenReportAPI{ - JSONCodec: &FakeCodec{EncodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleOpenReportAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleOpenReportAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleOpenReportAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleOpenReportAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestOpenReportWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleOpenReportAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleOpenReport struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.OpenReportResponse - url *url.URL - userAgent string -} - -func (h *handleOpenReport) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.OpenReportResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestOpenReportRoundTrip(t *testing.T) { - // setup - handler := &handleOpenReport{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleOpenReportAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.OpenReportRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -func TestSubmitMeasurementInvalidURL(t *testing.T) { - api := &simpleSubmitMeasurementAPI{ - BaseURL: "\t", // invalid - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if err == nil || !strings.HasSuffix(err.Error(), "invalid control character in URL") { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWithHTTPErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Err: errMocked} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementMarshalErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleSubmitMeasurementAPI{ - JSONCodec: &FakeCodec{EncodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWithNewRequestErr(t *testing.T) { - errMocked := errors.New("mocked error") - api := &simpleSubmitMeasurementAPI{ - RequestMaker: &FakeRequestMaker{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWith401(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrUnauthorized) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWith400(t *testing.T) { - clnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWithResponseBodyReadErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Err: errMocked}, - }} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -func TestSubmitMeasurementWithUnmarshalFailure(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 200, - Body: &FakeBody{Data: []byte(`{}`)}, - }} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} - -type handleSubmitMeasurement struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.SubmitMeasurementResponse - url *url.URL - userAgent string -} - -func (h *handleSubmitMeasurement) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.SubmitMeasurementResponse - ff := fakeFill{} - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestSubmitMeasurementRoundTrip(t *testing.T) { - // setup - handler := &handleSubmitMeasurement{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(&req) - api := &simpleSubmitMeasurementAPI{BaseURL: srvr.URL} - ff.Fill(&api.UserAgent) - // issue request - ctx := context.Background() - resp, err := api.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != api.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.SubmitMeasurementRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -func TestSubmitMeasurementTemplateErr(t *testing.T) { - errMocked := errors.New("mocked error") - clnt := &FakeHTTPClient{Resp: &http.Response{ - StatusCode: 500, - }} - api := &simpleSubmitMeasurementAPI{ - HTTPClient: clnt, - TemplateExecutor: &FakeTemplateExecutor{Err: errMocked}, - } - ctx := context.Background() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(req) - resp, err := api.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil resp") - } -} diff --git a/internal/ooapi/caching.go b/internal/ooapi/caching.go deleted file mode 100644 index 4624544..0000000 --- a/internal/ooapi/caching.go +++ /dev/null @@ -1,98 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:47.186361 +0200 CEST m=+0.000721751 - -package ooapi - -//go:generate go run ./internal/generator -file caching.go - -import ( - "context" - "reflect" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -// withCacheMeasurementMetaAPI implements caching for simpleMeasurementMetaAPI. -type withCacheMeasurementMetaAPI struct { - API callerForMeasurementMetaAPI // mandatory - GobCodec GobCodec // optional - KVStore KVStore // mandatory -} - -type cacheEntryForMeasurementMetaAPI struct { - Req *apimodel.MeasurementMetaRequest - Resp *apimodel.MeasurementMetaResponse -} - -// Call calls the API and implements caching. -func (c *withCacheMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) { - if resp, _ := c.readcache(req); resp != nil { - return resp, nil - } - resp, err := c.API.Call(ctx, req) - if err != nil { - return nil, err - } - if err := c.writecache(req, resp); err != nil { - return nil, err - } - return resp, nil -} - -func (c *withCacheMeasurementMetaAPI) gobCodec() GobCodec { - if c.GobCodec != nil { - return c.GobCodec - } - return &defaultGobCodec{} -} - -func (c *withCacheMeasurementMetaAPI) getcache() ([]cacheEntryForMeasurementMetaAPI, error) { - data, err := c.KVStore.Get("MeasurementMeta.cache") - if err != nil { - return nil, err - } - var out []cacheEntryForMeasurementMetaAPI - if err := c.gobCodec().Decode(data, &out); err != nil { - return nil, err - } - return out, nil -} - -func (c *withCacheMeasurementMetaAPI) setcache(in []cacheEntryForMeasurementMetaAPI) error { - data, err := c.gobCodec().Encode(in) - if err != nil { - return err - } - return c.KVStore.Set("MeasurementMeta.cache", data) -} - -func (c *withCacheMeasurementMetaAPI) readcache(req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) { - cache, err := c.getcache() - if err != nil { - return nil, err - } - for _, cur := range cache { - if reflect.DeepEqual(req, cur.Req) { - return cur.Resp, nil - } - } - return nil, errCacheNotFound -} - -func (c *withCacheMeasurementMetaAPI) writecache(req *apimodel.MeasurementMetaRequest, resp *apimodel.MeasurementMetaResponse) error { - cache, _ := c.getcache() - out := []cacheEntryForMeasurementMetaAPI{{Req: req, Resp: resp}} - const toomany = 64 - for idx, cur := range cache { - if reflect.DeepEqual(req, cur.Req) { - continue // we already updated the cache - } - if idx > toomany { - break - } - out = append(out, cur) - } - return c.setcache(out) -} - -var _ callerForMeasurementMetaAPI = &withCacheMeasurementMetaAPI{} diff --git a/internal/ooapi/caching_test.go b/internal/ooapi/caching_test.go deleted file mode 100644 index cb03290..0000000 --- a/internal/ooapi/caching_test.go +++ /dev/null @@ -1,223 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:47.604277 +0200 CEST m=+0.000796918 - -package ooapi - -//go:generate go run ./internal/generator -file caching_test.go - -import ( - "context" - "errors" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func TestCachesimpleMeasurementMetaAPISuccess(t *testing.T) { - ff := &fakeFill{} - var expect *apimodel.MeasurementMetaResponse - ff.Fill(&expect) - cache := &withCacheMeasurementMetaAPI{ - API: &FakeMeasurementMetaAPI{ - Response: expect, - }, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := cache.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } -} - -func TestCachesimpleMeasurementMetaAPIWriteCacheError(t *testing.T) { - errMocked := errors.New("mocked error") - ff := &fakeFill{} - var expect *apimodel.MeasurementMetaResponse - ff.Fill(&expect) - cache := &withCacheMeasurementMetaAPI{ - API: &FakeMeasurementMetaAPI{ - Response: expect, - }, - KVStore: &FakeKVStore{SetError: errMocked}, - } - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := cache.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } -} - -func TestCachesimpleMeasurementMetaAPIFailureWithNoCache(t *testing.T) { - errMocked := errors.New("mocked error") - ff := &fakeFill{} - cache := &withCacheMeasurementMetaAPI{ - API: &FakeMeasurementMetaAPI{ - Err: errMocked, - }, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := cache.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } -} - -func TestCachesimpleMeasurementMetaAPIFailureWithPreviousCache(t *testing.T) { - ff := &fakeFill{} - var expect *apimodel.MeasurementMetaResponse - ff.Fill(&expect) - fakeapi := &FakeMeasurementMetaAPI{ - Response: expect, - } - cache := &withCacheMeasurementMetaAPI{ - API: fakeapi, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - ctx := context.Background() - // first pass with no error at all - // use a separate scope to be sure we avoid mistakes - { - resp, err := cache.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - } - // second pass with failure - errMocked := errors.New("mocked error") - fakeapi.Err = errMocked - fakeapi.Response = nil - resp2, err := cache.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp2 == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp2); diff != "" { - t.Fatal(diff) - } -} - -func TestCachesimpleMeasurementMetaAPISetcacheWithEncodeError(t *testing.T) { - ff := &fakeFill{} - errMocked := errors.New("mocked error") - var in []cacheEntryForMeasurementMetaAPI - ff.Fill(&in) - cache := &withCacheMeasurementMetaAPI{ - GobCodec: &FakeCodec{EncodeErr: errMocked}, - } - err := cache.setcache(in) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } -} - -func TestCachesimpleMeasurementMetaAPIReadCacheNotFound(t *testing.T) { - ff := &fakeFill{} - var incache []cacheEntryForMeasurementMetaAPI - ff.Fill(&incache) - cache := &withCacheMeasurementMetaAPI{ - KVStore: &kvstore.Memory{}, - } - err := cache.setcache(incache) - if err != nil { - t.Fatal(err) - } - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - out, err := cache.readcache(req) - if !errors.Is(err, errCacheNotFound) { - t.Fatal("not the error we expected", err) - } - if out != nil { - t.Fatal("expected nil here") - } -} - -func TestCachesimpleMeasurementMetaAPIWriteCacheDuplicate(t *testing.T) { - ff := &fakeFill{} - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - var resp1 *apimodel.MeasurementMetaResponse - ff.Fill(&resp1) - var resp2 *apimodel.MeasurementMetaResponse - ff.Fill(&resp2) - cache := &withCacheMeasurementMetaAPI{ - KVStore: &kvstore.Memory{}, - } - err := cache.writecache(req, resp1) - if err != nil { - t.Fatal(err) - } - err = cache.writecache(req, resp2) - if err != nil { - t.Fatal(err) - } - out, err := cache.readcache(req) - if err != nil { - t.Fatal(err) - } - if out == nil { - t.Fatal("expected non-nil here") - } - if diff := cmp.Diff(resp2, out); diff != "" { - t.Fatal(diff) - } -} - -func TestCachesimpleMeasurementMetaAPICacheSizeLimited(t *testing.T) { - ff := &fakeFill{} - cache := &withCacheMeasurementMetaAPI{ - KVStore: &kvstore.Memory{}, - } - var prev int - for { - var req *apimodel.MeasurementMetaRequest - ff.Fill(&req) - var resp *apimodel.MeasurementMetaResponse - ff.Fill(&resp) - err := cache.writecache(req, resp) - if err != nil { - t.Fatal(err) - } - out, err := cache.getcache() - if err != nil { - t.Fatal(err) - } - if len(out) > prev { - prev = len(out) - continue - } - break - } -} diff --git a/internal/ooapi/callers.go b/internal/ooapi/callers.go deleted file mode 100644 index 85bb6e9..0000000 --- a/internal/ooapi/callers.go +++ /dev/null @@ -1,78 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:48.00142 +0200 CEST m=+0.000791126 - -package ooapi - -//go:generate go run ./internal/generator -file callers.go - -import ( - "context" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -// callerForCheckReportIDAPI represents any type exposing a method -// like simpleCheckReportIDAPI.Call. -type callerForCheckReportIDAPI interface { - Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error) -} - -// callerForCheckInAPI represents any type exposing a method -// like simpleCheckInAPI.Call. -type callerForCheckInAPI interface { - Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error) -} - -// callerForLoginAPI represents any type exposing a method -// like simpleLoginAPI.Call. -type callerForLoginAPI interface { - Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error) -} - -// callerForMeasurementMetaAPI represents any type exposing a method -// like simpleMeasurementMetaAPI.Call. -type callerForMeasurementMetaAPI interface { - Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) -} - -// callerForRegisterAPI represents any type exposing a method -// like simpleRegisterAPI.Call. -type callerForRegisterAPI interface { - Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error) -} - -// callerForTestHelpersAPI represents any type exposing a method -// like simpleTestHelpersAPI.Call. -type callerForTestHelpersAPI interface { - Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error) -} - -// callerForPsiphonConfigAPI represents any type exposing a method -// like simplePsiphonConfigAPI.Call. -type callerForPsiphonConfigAPI interface { - Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) -} - -// callerForTorTargetsAPI represents any type exposing a method -// like simpleTorTargetsAPI.Call. -type callerForTorTargetsAPI interface { - Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) -} - -// callerForURLsAPI represents any type exposing a method -// like simpleURLsAPI.Call. -type callerForURLsAPI interface { - Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error) -} - -// callerForOpenReportAPI represents any type exposing a method -// like simpleOpenReportAPI.Call. -type callerForOpenReportAPI interface { - Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error) -} - -// callerForSubmitMeasurementAPI represents any type exposing a method -// like simpleSubmitMeasurementAPI.Call. -type callerForSubmitMeasurementAPI interface { - Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error) -} diff --git a/internal/ooapi/client.go b/internal/ooapi/client.go deleted file mode 100644 index 693531f..0000000 --- a/internal/ooapi/client.go +++ /dev/null @@ -1,18 +0,0 @@ -package ooapi - -// Client is a client for speaking with the OONI API. Make sure you -// fill in the mandatory fields. -type Client struct { - // KVStore is the MANDATORY key-value store. You can use - // the kvstore.Memory{} struct for an in-memory store. - KVStore KVStore - - // The following fields are optional. When they are empty - // we will fallback to sensible defaults. - BaseURL string - GobCodec GobCodec - HTTPClient HTTPClient - JSONCodec JSONCodec - RequestMaker RequestMaker - UserAgent string -} diff --git a/internal/ooapi/clientcall.go b/internal/ooapi/clientcall.go deleted file mode 100644 index 7fb4852..0000000 --- a/internal/ooapi/clientcall.go +++ /dev/null @@ -1,214 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:48.430212 +0200 CEST m=+0.000888918 - -package ooapi - -//go:generate go run ./internal/generator -file clientcall.go - -import ( - "context" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func (c *Client) newCheckReportIDCaller() callerForCheckReportIDAPI { - return &simpleCheckReportIDAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// CheckReportID calls the CheckReportID API. -func (c *Client) CheckReportID( - ctx context.Context, req *apimodel.CheckReportIDRequest, -) (*apimodel.CheckReportIDResponse, error) { - api := c.newCheckReportIDCaller() - return api.Call(ctx, req) -} - -func (c *Client) newCheckInCaller() callerForCheckInAPI { - return &simpleCheckInAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// CheckIn calls the CheckIn API. -func (c *Client) CheckIn( - ctx context.Context, req *apimodel.CheckInRequest, -) (*apimodel.CheckInResponse, error) { - api := c.newCheckInCaller() - return api.Call(ctx, req) -} - -func (c *Client) newMeasurementMetaCaller() callerForMeasurementMetaAPI { - return &withCacheMeasurementMetaAPI{ - API: &simpleMeasurementMetaAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - GobCodec: c.GobCodec, - KVStore: c.KVStore, - } -} - -// MeasurementMeta calls the MeasurementMeta API. -func (c *Client) MeasurementMeta( - ctx context.Context, req *apimodel.MeasurementMetaRequest, -) (*apimodel.MeasurementMetaResponse, error) { - api := c.newMeasurementMetaCaller() - return api.Call(ctx, req) -} - -func (c *Client) newTestHelpersCaller() callerForTestHelpersAPI { - return &simpleTestHelpersAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// TestHelpers calls the TestHelpers API. -func (c *Client) TestHelpers( - ctx context.Context, req *apimodel.TestHelpersRequest, -) (apimodel.TestHelpersResponse, error) { - api := c.newTestHelpersCaller() - return api.Call(ctx, req) -} - -func (c *Client) newPsiphonConfigCaller() callerForPsiphonConfigAPI { - return &withLoginPsiphonConfigAPI{ - API: &simplePsiphonConfigAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - JSONCodec: c.JSONCodec, - KVStore: c.KVStore, - RegisterAPI: &simpleRegisterAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - LoginAPI: &simpleLoginAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - } -} - -// PsiphonConfig calls the PsiphonConfig API. -func (c *Client) PsiphonConfig( - ctx context.Context, req *apimodel.PsiphonConfigRequest, -) (apimodel.PsiphonConfigResponse, error) { - api := c.newPsiphonConfigCaller() - return api.Call(ctx, req) -} - -func (c *Client) newTorTargetsCaller() callerForTorTargetsAPI { - return &withLoginTorTargetsAPI{ - API: &simpleTorTargetsAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - JSONCodec: c.JSONCodec, - KVStore: c.KVStore, - RegisterAPI: &simpleRegisterAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - LoginAPI: &simpleLoginAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - }, - } -} - -// TorTargets calls the TorTargets API. -func (c *Client) TorTargets( - ctx context.Context, req *apimodel.TorTargetsRequest, -) (apimodel.TorTargetsResponse, error) { - api := c.newTorTargetsCaller() - return api.Call(ctx, req) -} - -func (c *Client) newURLsCaller() callerForURLsAPI { - return &simpleURLsAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// URLs calls the URLs API. -func (c *Client) URLs( - ctx context.Context, req *apimodel.URLsRequest, -) (*apimodel.URLsResponse, error) { - api := c.newURLsCaller() - return api.Call(ctx, req) -} - -func (c *Client) newOpenReportCaller() callerForOpenReportAPI { - return &simpleOpenReportAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// OpenReport calls the OpenReport API. -func (c *Client) OpenReport( - ctx context.Context, req *apimodel.OpenReportRequest, -) (*apimodel.OpenReportResponse, error) { - api := c.newOpenReportCaller() - return api.Call(ctx, req) -} - -func (c *Client) newSubmitMeasurementCaller() callerForSubmitMeasurementAPI { - return &simpleSubmitMeasurementAPI{ - BaseURL: c.BaseURL, - HTTPClient: c.HTTPClient, - JSONCodec: c.JSONCodec, - RequestMaker: c.RequestMaker, - UserAgent: c.UserAgent, - } -} - -// SubmitMeasurement calls the SubmitMeasurement API. -func (c *Client) SubmitMeasurement( - ctx context.Context, req *apimodel.SubmitMeasurementRequest, -) (*apimodel.SubmitMeasurementResponse, error) { - api := c.newSubmitMeasurementCaller() - return api.Call(ctx, req) -} diff --git a/internal/ooapi/clientcall_test.go b/internal/ooapi/clientcall_test.go deleted file mode 100644 index ef8ff5e..0000000 --- a/internal/ooapi/clientcall_test.go +++ /dev/null @@ -1,899 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:48.846389 +0200 CEST m=+0.000695543 - -package ooapi - -//go:generate go run ./internal/generator -file clientcall_test.go - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/url" - "sync" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -type handleClientCallCheckReportID struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.CheckReportIDResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallCheckReportID) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.CheckReportIDResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestCheckReportIDClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallCheckReportID{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.CheckReportIDRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.CheckReportID(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simpleCheckReportIDAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallCheckIn struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.CheckInResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallCheckIn) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.CheckInResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestCheckInClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallCheckIn{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.CheckInRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.CheckIn(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.CheckInRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallMeasurementMeta struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.MeasurementMetaResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallMeasurementMeta) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.MeasurementMetaResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestMeasurementMetaClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallMeasurementMeta{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.MeasurementMetaRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.MeasurementMeta(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simpleMeasurementMetaAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallTestHelpers struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.TestHelpersResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallTestHelpers) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.TestHelpersResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestTestHelpersClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallTestHelpers{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.TestHelpersRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.TestHelpers(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simpleTestHelpersAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallPsiphonConfig struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.PsiphonConfigResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallPsiphonConfig) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - if r.URL.Path == "/api/v1/register" { - var out apimodel.RegisterResponse - ff.Fill(&out) - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) - return - } - if r.URL.Path == "/api/v1/login" { - var out apimodel.LoginResponse - ff.Fill(&out) - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) - return - } - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.PsiphonConfigResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestPsiphonConfigClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallPsiphonConfig{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.PsiphonConfigRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.PsiphonConfig(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simplePsiphonConfigAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallTorTargets struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp apimodel.TorTargetsResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallTorTargets) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - if r.URL.Path == "/api/v1/register" { - var out apimodel.RegisterResponse - ff.Fill(&out) - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) - return - } - if r.URL.Path == "/api/v1/login" { - var out apimodel.LoginResponse - ff.Fill(&out) - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) - return - } - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out apimodel.TorTargetsResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestTorTargetsClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallTorTargets{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.TorTargetsRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.TorTargets(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simpleTorTargetsAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallURLs struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.URLsResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallURLs) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.URLsResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestURLsClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallURLs{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.URLsRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.URLs(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "GET" { - t.Fatal("invalid method") - } - // check the query - api := &simpleURLsAPI{BaseURL: srvr.URL} - httpReq, err := api.newRequest(context.Background(), req) - if err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != "" { - t.Fatal(diff) - } - if diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallOpenReport struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.OpenReportResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallOpenReport) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.OpenReportResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestOpenReportClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallOpenReport{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.OpenReportRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.OpenReport(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.OpenReportRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} - -type handleClientCallSubmitMeasurement struct { - accept string - body []byte - contentType string - count int32 - method string - mu sync.Mutex - resp *apimodel.SubmitMeasurementResponse - url *url.URL - userAgent string -} - -func (h *handleClientCallSubmitMeasurement) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ff := fakeFill{} - defer h.mu.Unlock() - h.mu.Lock() - if h.count > 0 { - w.WriteHeader(400) - return - } - h.count++ - if r.Body != nil { - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - h.body = data - } - h.method = r.Method - h.url = r.URL - h.accept = r.Header.Get("Accept") - h.contentType = r.Header.Get("Content-Type") - h.userAgent = r.Header.Get("User-Agent") - var out *apimodel.SubmitMeasurementResponse - ff.Fill(&out) - h.resp = out - data, err := json.Marshal(out) - if err != nil { - w.WriteHeader(400) - return - } - w.Write(data) -} - -func TestSubmitMeasurementClientCallRoundTrip(t *testing.T) { - // setup - handler := &handleClientCallSubmitMeasurement{} - srvr := httptest.NewServer(handler) - defer srvr.Close() - req := &apimodel.SubmitMeasurementRequest{} - ff := &fakeFill{} - ff.Fill(&req) - clnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL} - ff.Fill(&clnt.UserAgent) - // issue request - ctx := context.Background() - resp, err := clnt.SubmitMeasurement(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response here") - } - // compare our response and server's one - if diff := cmp.Diff(handler.resp, resp); diff != "" { - t.Fatal(diff) - } - // check whether headers are OK - if handler.accept != "application/json" { - t.Fatal("invalid accept header") - } - if handler.userAgent != clnt.UserAgent { - t.Fatal("invalid user-agent header") - } - // check whether the method is OK - if handler.method != "POST" { - t.Fatal("invalid method") - } - // check the body - if handler.contentType != "application/json" { - t.Fatal("invalid content-type header") - } - got := &apimodel.SubmitMeasurementRequest{} - if err := json.Unmarshal(handler.body, &got); err != nil { - t.Fatal(err) - } - if diff := cmp.Diff(req, got); diff != "" { - t.Fatal(diff) - } -} diff --git a/internal/ooapi/cloners.go b/internal/ooapi/cloners.go deleted file mode 100644 index c66d76a..0000000 --- a/internal/ooapi/cloners.go +++ /dev/null @@ -1,18 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:49.209515 +0200 CEST m=+0.000689210 - -package ooapi - -//go:generate go run ./internal/generator -file cloners.go - -// clonerForPsiphonConfigAPI represents any type exposing a method -// like simplePsiphonConfigAPI.WithToken. -type clonerForPsiphonConfigAPI interface { - WithToken(token string) callerForPsiphonConfigAPI -} - -// clonerForTorTargetsAPI represents any type exposing a method -// like simpleTorTargetsAPI.WithToken. -type clonerForTorTargetsAPI interface { - WithToken(token string) callerForTorTargetsAPI -} diff --git a/internal/ooapi/default.go b/internal/ooapi/default.go deleted file mode 100644 index 9917760..0000000 --- a/internal/ooapi/default.go +++ /dev/null @@ -1,57 +0,0 @@ -package ooapi - -import ( - "bytes" - "context" - "encoding/gob" - "encoding/json" - "io" - "net/http" - "strings" - "text/template" -) - -type defaultRequestMaker struct{} - -func (*defaultRequestMaker) NewRequest( - ctx context.Context, method, URL string, body io.Reader) (*http.Request, error) { - return http.NewRequestWithContext(ctx, method, URL, body) -} - -type defaultJSONCodec struct{} - -func (*defaultJSONCodec) Encode(v interface{}) ([]byte, error) { - return json.Marshal(v) -} - -func (*defaultJSONCodec) Decode(b []byte, v interface{}) error { - return json.Unmarshal(b, v) -} - -type defaultTemplateExecutor struct{} - -func (*defaultTemplateExecutor) Execute(tmpl string, v interface{}) (string, error) { - to, err := template.New("t").Parse(tmpl) - if err != nil { - return "", err - } - var sb strings.Builder - if err := to.Execute(&sb, v); err != nil { - return "", err - } - return sb.String(), nil -} - -type defaultGobCodec struct{} - -func (*defaultGobCodec) Encode(v interface{}) ([]byte, error) { - var bb bytes.Buffer - if err := gob.NewEncoder(&bb).Encode(v); err != nil { - return nil, err - } - return bb.Bytes(), nil -} - -func (*defaultGobCodec) Decode(b []byte, v interface{}) error { - return gob.NewDecoder(bytes.NewReader(b)).Decode(v) -} diff --git a/internal/ooapi/default_test.go b/internal/ooapi/default_test.go deleted file mode 100644 index 930181f..0000000 --- a/internal/ooapi/default_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package ooapi - -import ( - "strings" - "testing" -) - -func TestDefaultTemplateExecutorParseError(t *testing.T) { - te := &defaultTemplateExecutor{} - out, err := te.Execute("{{ .Foo", nil) - if err == nil || !strings.HasSuffix(err.Error(), "unclosed action") { - t.Fatal("not the error we expected", err) - } - if out != "" { - t.Fatal("expected empty string") - } -} - -func TestDefaultTemplateExecutorExecError(t *testing.T) { - te := &defaultTemplateExecutor{} - arg := make(chan interface{}) - out, err := te.Execute("{{ .Foo }}", arg) - if err == nil || !strings.Contains(err.Error(), `can't evaluate field Foo`) { - t.Fatal("not the error we expected", err) - } - if out != "" { - t.Fatal("expected empty string") - } -} - -func TestDefaultGobCodecEncodeError(t *testing.T) { - codec := &defaultGobCodec{} - arg := make(chan interface{}) - data, err := codec.Encode(arg) - if err == nil || !strings.Contains(err.Error(), "can't handle type") { - t.Fatal("not the error we expected", err) - } - if data != nil { - t.Fatal("expected nil data") - } -} diff --git a/internal/ooapi/dependencies.go b/internal/ooapi/dependencies.go deleted file mode 100644 index b2c56db..0000000 --- a/internal/ooapi/dependencies.go +++ /dev/null @@ -1,59 +0,0 @@ -package ooapi - -import ( - "context" - "io" - "net/http" - - "github.com/ooni/probe-cli/v3/internal/model" -) - -// JSONCodec is a JSON encoder and decoder. Generally, we use a -// default JSONCodec in Client. This is the interface to implement -// if you want to override such a default. -type JSONCodec interface { - // Encode encodes v as a serialized JSON byte slice. - Encode(v interface{}) ([]byte, error) - - // Decode decodes the serialized JSON byte slice into v. - Decode(b []byte, v interface{}) error -} - -// RequestMaker makes an HTTP request. Generally, we use a -// default RequestMaker in Client. This is the interface to implement -// if you want to override such a default. -type RequestMaker interface { - // NewRequest creates a new HTTP request. - NewRequest(ctx context.Context, method, URL string, body io.Reader) (*http.Request, error) -} - -// templateExecutor parses and executes a text template. -type templateExecutor interface { - // Execute takes in input a template string and some piece of data. It - // returns either a string where template parameters have been replaced, - // on success, or an error, on failure. - Execute(tmpl string, v interface{}) (string, error) -} - -// HTTPClient is the interface of a generic HTTP client. The -// stdlib's http.Client implements this interface. We use -// http.DefaultClient as the default HTTPClient used by Client. -// Consumers of this package typically provide a custom HTTPClient -// with additional functionality (e.g., DoH, circumvention). -type HTTPClient = model.HTTPClient - -// GobCodec is a Gob encoder and decoder. Generally, we use a -// default GobCodec in Client. This is the interface to implement -// if you want to override such a default. -type GobCodec interface { - // Encode encodes v as a serialized gob byte slice. - Encode(v interface{}) ([]byte, error) - - // Decode decodes the serialized gob byte slice into v. - Decode(b []byte, v interface{}) error -} - -// KVStore is a key-value store. This is the interface the -// client expect for the key-value store used to save persistent -// state (typically on the file system). -type KVStore = model.KeyValueStore diff --git a/internal/ooapi/doc.go b/internal/ooapi/doc.go deleted file mode 100644 index 42cb218..0000000 --- a/internal/ooapi/doc.go +++ /dev/null @@ -1,58 +0,0 @@ -// Package ooapi contains a client for the OONI API. We -// automatically generate the code in this package from the -// apimodel and internal/generator packages. -// -// Note -// -// This package is currrently unused. We plan on replacing -// existing code to speak with the OONI API with it. -// -// Usage -// -// You need to create a Client. Make sure you set all -// the mandatory fields. You will then have a function -// for every supported OONI API. This function will -// take in input a context and a request. You need to -// fill the request, of course. The return value is -// either a response or an error. -// -// If an API requires login, we will automatically -// perform the login. If an API uses caching, we will -// automatically use the cache. -// -// Design -// -// Most of the code in this package is auto-generated from the -// data model in ./apimodel and the definition of APIs provided -// by ./internal/generator/spec.go. -// -// We keep the generated files up-to-date by running -// -// go generate ./... -// -// We have tests that ensure that the definition of the API -// used here is reasonably close to the server's one. -// -// Testing -// -// The following command -// -// go test ./... -// -// will, among other things, ensure that the our API spec -// is consistent with the server's one. Running -// -// go test -short ./... -// -// will exclude most (slow) integration tests. -// -// Architecture -// -// The ./apimodel sub-package contains the definition of request -// and response messages. We rely on tagging to specify how -// we should encode and decode messages. -// -// The ./internal/generator sub-package contains code to generate most -// code in this package. In particular, the spec.go file is -// the specification of the APIs. -package ooapi diff --git a/internal/ooapi/errors.go b/internal/ooapi/errors.go deleted file mode 100644 index 5227ed4..0000000 --- a/internal/ooapi/errors.go +++ /dev/null @@ -1,14 +0,0 @@ -package ooapi - -import "errors" - -// Errors defined by this package. -var ( - ErrAPICallFailed = errors.New("ooapi: API call failed") - ErrEmptyField = errors.New("ooapi: empty field") - ErrHTTPFailure = errors.New("ooapi: http request failed") - ErrJSONLiteralNull = errors.New("ooapi: server returned us a literal null") - ErrMissingToken = errors.New("ooapi: missing auth token") - ErrUnauthorized = errors.New("ooapi: not authorized") - errCacheNotFound = errors.New("ooapi: not found in cache") -) diff --git a/internal/ooapi/fake_test.go b/internal/ooapi/fake_test.go deleted file mode 100644 index 31d610e..0000000 --- a/internal/ooapi/fake_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package ooapi - -import ( - "context" - "io" - "net/http" - "time" - - "github.com/ooni/probe-cli/v3/internal/fakefill" - "github.com/ooni/probe-cli/v3/internal/netxlite" -) - -// fakeFill forwards the fakefill.Filler type -type fakeFill = fakefill.Filler - -type FakeCodec struct { - DecodeErr error - EncodeData []byte - EncodeErr error -} - -func (mc *FakeCodec) Encode(v interface{}) ([]byte, error) { - return mc.EncodeData, mc.EncodeErr -} - -func (mc *FakeCodec) Decode(b []byte, v interface{}) error { - return mc.DecodeErr -} - -type FakeHTTPClient struct { - Err error - Resp *http.Response -} - -func (c *FakeHTTPClient) Do(req *http.Request) (*http.Response, error) { - time.Sleep(10 * time.Microsecond) - if req.Body != nil { - _, _ = netxlite.ReadAllContext(req.Context(), req.Body) - req.Body.Close() - } - if c.Err != nil { - return nil, c.Err - } - c.Resp.Request = req // non thread safe but it doesn't matter - return c.Resp, nil -} - -func (c *FakeHTTPClient) CloseIdleConnections() {} - -type FakeBody struct { - Data []byte - Err error -} - -func (fb *FakeBody) Read(p []byte) (int, error) { - time.Sleep(10 * time.Microsecond) - if fb.Err != nil { - return 0, fb.Err - } - if len(fb.Data) <= 0 { - return 0, io.EOF - } - n := copy(p, fb.Data) - fb.Data = fb.Data[n:] - return n, nil -} - -func (fb *FakeBody) Close() error { - return nil -} - -type FakeRequestMaker struct { - Req *http.Request - Err error -} - -func (frm *FakeRequestMaker) NewRequest( - ctx context.Context, method, URL string, body io.Reader) (*http.Request, error) { - return frm.Req, frm.Err -} - -type FakeTemplateExecutor struct { - Out string - Err error -} - -func (fte *FakeTemplateExecutor) Execute(tmpl string, v interface{}) (string, error) { - return fte.Out, fte.Err -} - -type FakeKVStore struct { - SetError error - GetData []byte - GetError error -} - -func (fs *FakeKVStore) Get(key string) ([]byte, error) { - return fs.GetData, fs.GetError -} - -func (fs *FakeKVStore) Set(key string, value []byte) error { - return fs.SetError -} diff --git a/internal/ooapi/fakeapi_test.go b/internal/ooapi/fakeapi_test.go deleted file mode 100644 index b9ccc4e..0000000 --- a/internal/ooapi/fakeapi_test.go +++ /dev/null @@ -1,212 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:49.546037 +0200 CEST m=+0.000695501 - -package ooapi - -//go:generate go run ./internal/generator -file fakeapi_test.go - -import ( - "context" - - "github.com/ooni/probe-cli/v3/internal/atomicx" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -type FakeCheckReportIDAPI struct { - Err error - Response *apimodel.CheckReportIDResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeCheckReportIDAPI) Call(ctx context.Context, req *apimodel.CheckReportIDRequest) (*apimodel.CheckReportIDResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForCheckReportIDAPI = &FakeCheckReportIDAPI{} -) - -type FakeCheckInAPI struct { - Err error - Response *apimodel.CheckInResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeCheckInAPI) Call(ctx context.Context, req *apimodel.CheckInRequest) (*apimodel.CheckInResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForCheckInAPI = &FakeCheckInAPI{} -) - -type FakeLoginAPI struct { - Err error - Response *apimodel.LoginResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeLoginAPI) Call(ctx context.Context, req *apimodel.LoginRequest) (*apimodel.LoginResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForLoginAPI = &FakeLoginAPI{} -) - -type FakeMeasurementMetaAPI struct { - Err error - Response *apimodel.MeasurementMetaResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeMeasurementMetaAPI) Call(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*apimodel.MeasurementMetaResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForMeasurementMetaAPI = &FakeMeasurementMetaAPI{} -) - -type FakeRegisterAPI struct { - Err error - Response *apimodel.RegisterResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeRegisterAPI) Call(ctx context.Context, req *apimodel.RegisterRequest) (*apimodel.RegisterResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForRegisterAPI = &FakeRegisterAPI{} -) - -type FakeTestHelpersAPI struct { - Err error - Response apimodel.TestHelpersResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeTestHelpersAPI) Call(ctx context.Context, req *apimodel.TestHelpersRequest) (apimodel.TestHelpersResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForTestHelpersAPI = &FakeTestHelpersAPI{} -) - -type FakePsiphonConfigAPI struct { - WithResult callerForPsiphonConfigAPI - Err error - Response apimodel.PsiphonConfigResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakePsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -func (fapi *FakePsiphonConfigAPI) WithToken(token string) callerForPsiphonConfigAPI { - return fapi.WithResult -} - -var ( - _ callerForPsiphonConfigAPI = &FakePsiphonConfigAPI{} - _ clonerForPsiphonConfigAPI = &FakePsiphonConfigAPI{} -) - -type FakeTorTargetsAPI struct { - WithResult callerForTorTargetsAPI - Err error - Response apimodel.TorTargetsResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -func (fapi *FakeTorTargetsAPI) WithToken(token string) callerForTorTargetsAPI { - return fapi.WithResult -} - -var ( - _ callerForTorTargetsAPI = &FakeTorTargetsAPI{} - _ clonerForTorTargetsAPI = &FakeTorTargetsAPI{} -) - -type FakeURLsAPI struct { - Err error - Response *apimodel.URLsResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeURLsAPI) Call(ctx context.Context, req *apimodel.URLsRequest) (*apimodel.URLsResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForURLsAPI = &FakeURLsAPI{} -) - -type FakeOpenReportAPI struct { - Err error - Response *apimodel.OpenReportResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeOpenReportAPI) Call(ctx context.Context, req *apimodel.OpenReportRequest) (*apimodel.OpenReportResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForOpenReportAPI = &FakeOpenReportAPI{} -) - -type FakeSubmitMeasurementAPI struct { - Err error - Response *apimodel.SubmitMeasurementResponse - CountCall *atomicx.Int64 -} - -func (fapi *FakeSubmitMeasurementAPI) Call(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*apimodel.SubmitMeasurementResponse, error) { - if fapi.CountCall != nil { - fapi.CountCall.Add(1) - } - return fapi.Response, fapi.Err -} - -var ( - _ callerForSubmitMeasurementAPI = &FakeSubmitMeasurementAPI{} -) diff --git a/internal/ooapi/httpclient_test.go b/internal/ooapi/httpclient_test.go deleted file mode 100644 index 466d49f..0000000 --- a/internal/ooapi/httpclient_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package ooapi - -import ( - "net/http" - "testing" -) - -type VerboseHTTPClient struct { - T *testing.T -} - -func (c *VerboseHTTPClient) Do(req *http.Request) (*http.Response, error) { - c.T.Logf("> %s %s", req.Method, req.URL.String()) - resp, err := http.DefaultClient.Do(req) - if err != nil { - c.T.Logf("< %s", err.Error()) - return nil, err - } - c.T.Logf("< %d", resp.StatusCode) - return resp, nil -} - -func (c *VerboseHTTPClient) CloseIdleConnections() {} diff --git a/internal/ooapi/integration_test.go b/internal/ooapi/integration_test.go deleted file mode 100644 index 0fca2f7..0000000 --- a/internal/ooapi/integration_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package ooapi_test - -import ( - "context" - "testing" - - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/model" - "github.com/ooni/probe-cli/v3/internal/ooapi" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func TestWithRealServerDoCheckIn(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.CheckInRequest{ - Charging: true, - OnWiFi: true, - Platform: "android", - ProbeASN: "AS12353", - ProbeCC: "IT", - RunType: model.RunTypeTimed, - SoftwareName: "ooniprobe-android", - SoftwareVersion: "2.7.1", - WebConnectivity: apimodel.CheckInRequestWebConnectivity{ - CategoryCodes: []string{"NEWS", "CULTR"}, - }, - } - httpClnt := &ooapi.VerboseHTTPClient{T: t} - clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.CheckIn(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - for idx, url := range resp.Tests.WebConnectivity.URLs { - if idx >= 3 { - break - } - t.Logf("- %+v", url) - } -} - -func TestWithRealServerDoCheckReportID(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.CheckReportIDRequest{ - ReportID: "20210223T093606Z_ndt_JO_8376_n1_kDYToqrugDY54Soy", - } - clnt := &ooapi.Client{KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.CheckReportID(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp) -} - -func TestWithRealServerDoMeasurementMeta(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.MeasurementMetaRequest{ - ReportID: "20210223T093606Z_ndt_JO_8376_n1_kDYToqrugDY54Soy", - } - clnt := &ooapi.Client{KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.MeasurementMeta(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp) -} - -func TestWithRealServerDoOpenReport(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.OpenReportRequest{ - DataFormatVersion: "0.2.0", - Format: "json", - ProbeASN: "AS137", - ProbeCC: "IT", - SoftwareName: "miniooni", - SoftwareVersion: "0.1.0-dev", - TestName: "example", - TestStartTime: "2018-11-01 15:33:20", - TestVersion: "0.1.0", - } - clnt := &ooapi.Client{KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.OpenReport(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp) -} - -func TestWithRealServerDoPsiphonConfig(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.PsiphonConfigRequest{} - httpClnt := &ooapi.VerboseHTTPClient{T: t} - clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.PsiphonConfig(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp != nil) -} - -func TestWithRealServerDoTorTargets(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.TorTargetsRequest{} - httpClnt := &ooapi.VerboseHTTPClient{T: t} - clnt := &ooapi.Client{HTTPClient: httpClnt, KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.TorTargets(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp != nil) -} - -func TestWithRealServerDoURLs(t *testing.T) { - if testing.Short() { - t.Skip("skip test in short mode") - } - req := &apimodel.URLsRequest{ - CountryCode: "IT", - Limit: 3, - } - clnt := &ooapi.Client{KVStore: &kvstore.Memory{}} - ctx := context.Background() - resp, err := clnt.URLs(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non nil pointer here") - } - t.Logf("%+v", resp) -} diff --git a/internal/ooapi/internal/generator/apis.go b/internal/ooapi/internal/generator/apis.go deleted file mode 100644 index cbaedd9..0000000 --- a/internal/ooapi/internal/generator/apis.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -// apiField contains the fields of an API data structure -type apiField struct { - // name is the field name - name string - - // kind is the filed type - kind string - - // comment is a brief comment to document the field - comment string - - // ifLogin indicates whether this field should only be - // emitted when the API requires login - ifLogin bool - - // ifTemplate indicates whether this field should only be - // emitted when the URL path is a template - ifTemplate bool - - // noClone is true when this field should not be copied - // from the parent data structure when cloning - noClone bool -} - -var apiFields = []apiField{{ - name: "BaseURL", - kind: "string", - comment: "optional", -}, { - name: "HTTPClient", - kind: "HTTPClient", - comment: "optional", -}, { - name: "JSONCodec", - kind: "JSONCodec", - comment: "optional", -}, { - name: "Token", - kind: "string", - comment: "mandatory", - ifLogin: true, - noClone: true, -}, { - name: "RequestMaker", - kind: "RequestMaker", - comment: "optional", -}, { - name: "TemplateExecutor", - kind: "templateExecutor", - comment: "optional", - ifTemplate: true, -}, { - name: "UserAgent", - kind: "string", - comment: "optional", -}} - -func (d *Descriptor) genNewAPI(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s implements the %s API.\n", d.APIStructName(), d.Name) - fmt.Fprintf(sb, "type %s struct {\n", d.APIStructName()) - for _, f := range apiFields { - if !d.RequiresLogin && f.ifLogin { - continue - } - if !d.URLPath.IsTemplate && f.ifTemplate { - continue - } - fmt.Fprintf(sb, "\t%s %s // %s\n", f.name, f.kind, f.comment) - } - fmt.Fprint(sb, "}\n\n") - - if d.RequiresLogin { - fmt.Fprintf(sb, "// WithToken returns a copy of the API where the\n") - fmt.Fprintf(sb, "// value of the Token field is replaced with token.\n") - fmt.Fprintf(sb, "func (api *%s) WithToken(token string) %s {\n", - d.APIStructName(), d.CallerInterfaceName()) - fmt.Fprintf(sb, "out := &%s{}\n", d.APIStructName()) - for _, f := range apiFields { - if !d.URLPath.IsTemplate && f.ifTemplate { - continue - } - if f.noClone == true { - continue - } - fmt.Fprintf(sb, "out.%s = api.%s\n", f.name, f.name) - } - fmt.Fprint(sb, "out.Token = token\n") - fmt.Fprint(sb, "return out\n") - fmt.Fprint(sb, "}\n\n") - } - - fmt.Fprintf(sb, "func (api *%s) baseURL() string {\n", d.APIStructName()) - fmt.Fprint(sb, "\tif api.BaseURL != \"\" {\n") - fmt.Fprint(sb, "\t\treturn api.BaseURL\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn \"https://ps1.ooni.io\"\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) requestMaker() RequestMaker {\n", d.APIStructName()) - fmt.Fprint(sb, "\tif api.RequestMaker != nil {\n") - fmt.Fprint(sb, "\t\treturn api.RequestMaker\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &defaultRequestMaker{}\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n", d.APIStructName()) - fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n") - fmt.Fprint(sb, "\t\treturn api.JSONCodec\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n") - fmt.Fprint(sb, "}\n\n") - - if d.URLPath.IsTemplate { - fmt.Fprintf( - sb, "func (api *%s) templateExecutor() templateExecutor {\n", - d.APIStructName()) - fmt.Fprint(sb, "\tif api.TemplateExecutor != nil {\n") - fmt.Fprint(sb, "\t\treturn api.TemplateExecutor\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &defaultTemplateExecutor{}\n") - fmt.Fprint(sb, "}\n\n") - } - - fmt.Fprintf( - sb, "func (api *%s) httpClient() HTTPClient {\n", - d.APIStructName()) - fmt.Fprint(sb, "\tif api.HTTPClient != nil {\n") - fmt.Fprint(sb, "\t\treturn api.HTTPClient\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn http.DefaultClient\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "// Call calls the %s API.\n", d.Name) - fmt.Fprintf( - sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n", - d.APIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "\thttpReq, err := api.newRequest(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\thttpReq.Header.Add(\"Accept\", \"application/json\")\n") - if d.RequiresLogin { - fmt.Fprint(sb, "\tif api.Token == \"\" {\n") - fmt.Fprint(sb, "\t\treturn nil, ErrMissingToken\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\thttpReq.Header.Add(\"Authorization\", newAuthorizationHeader(api.Token))\n") - } - fmt.Fprint(sb, "\tif api.UserAgent != \"\" {\n") - fmt.Fprint(sb, "\t\thttpReq.Header.Add(\"User-Agent\", api.UserAgent)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\thttpResp, err := api.httpClient().Do(httpReq)\n") - fmt.Fprint(sb, "\treturn api.newResponse(ctx, httpResp, err)\n") - fmt.Fprint(sb, "}\n\n") -} - -// GenAPIsGo generates apis.go. -func GenAPIsGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"net/http\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - desc.genNewAPI(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/apistest.go b/internal/ooapi/internal/generator/apistest.go deleted file mode 100644 index ba405e1..0000000 --- a/internal/ooapi/internal/generator/apistest.go +++ /dev/null @@ -1,461 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "strings" - "time" -) - -func (d *Descriptor) genTestNewRequest(sb *strings.Builder) { - fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\tff.Fill(req)\n") -} - -func (d *Descriptor) genTestInvalidURL(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sInvalidURL(t *testing.T) {\n", d.Name) - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tBaseURL: \"\\t\", // invalid\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err == nil || !strings.HasSuffix(err.Error(), \"invalid control character in URL\") {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithMissingToken(sb *strings.Builder) { - if d.RequiresLogin == false { - return // does not make sense when login isn't required - } - fmt.Fprintf(sb, "func Test%sWithMissingToken(t *testing.T) {\n", d.Name) - fmt.Fprintf(sb, "\tapi := &%s{} // no token\n", d.APIStructName()) - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrMissingToken) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithHTTPErr(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithHTTPErr(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Err: errMocked}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestMarshalErr(sb *strings.Builder) { - if d.Method != "POST" { - return // does not make sense when we don't send a request body - } - fmt.Fprintf(sb, "func Test%sMarshalErr(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{EncodeErr: errMocked},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithNewRequestErr(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithNewRequestErr(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tRequestMaker: &FakeRequestMaker{Err: errMocked},\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWith401(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWith401(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrUnauthorized) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWith400(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWith400(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithResponseBodyReadErr(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithResponseBodyReadErr(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n") - fmt.Fprint(sb, "\t\tStatusCode: 200,\n") - fmt.Fprint(sb, "\t\tBody: &FakeBody{Err: errMocked},\n") - fmt.Fprint(sb, "\t}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithUnmarshalFailure(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithUnmarshalFailure(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n") - fmt.Fprint(sb, "\t\tStatusCode: 200,\n") - fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`{}`)},\n") - fmt.Fprint(sb, "\t}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - fmt.Fprintf(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestRoundTrip(sb *strings.Builder) { - // generate the type of the handler - fmt.Fprintf(sb, "type handle%s struct {\n", d.Name) - fmt.Fprint(sb, "\taccept string\n") - fmt.Fprint(sb, "\tbody []byte\n") - fmt.Fprint(sb, "\tcontentType string\n") - fmt.Fprint(sb, "\tcount int32\n") - fmt.Fprint(sb, "\tmethod string\n") - fmt.Fprint(sb, "\tmu sync.Mutex\n") - fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\turl *url.URL\n") - fmt.Fprint(sb, "\tuserAgent string\n") - fmt.Fprint(sb, "}\n\n") - - // generate the handling function - fmt.Fprintf(sb, - "func (h *handle%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {", - d.Name) - fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n") - fmt.Fprint(sb, "\th.mu.Lock()\n") - fmt.Fprint(sb, "\tif h.count > 0 {\n") - fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n") - fmt.Fprint(sb, "\t\treturn\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\th.count++\n") - fmt.Fprint(sb, "\tif r.Body != nil {\n") - fmt.Fprint(sb, "\t\tdata, err := netxlite.ReadAllContext(r.Context(), r.Body)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\t\treturn\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\th.body = data\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\th.method = r.Method\n") - fmt.Fprint(sb, "\th.url = r.URL\n") - fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n") - fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n") - fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n") - fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff := fakeFill{}\n") - fmt.Fprint(sb, "\tff.Fill(&out)\n") - fmt.Fprintf(sb, "\th.resp = out\n") - fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n") - fmt.Fprintf(sb, "\tif err != nil {\n") - fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\treturn\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tw.Write(data)\n") - fmt.Fprintf(sb, "\t}\n\n") - - // generate the test itself - fmt.Fprintf(sb, "func Test%sRoundTrip(t *testing.T) {\n", d.Name) - - fmt.Fprint(sb, "\t// setup\n") - fmt.Fprintf(sb, "\thandler := &handle%s{}\n", d.Name) - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName()) - fmt.Fprint(sb, "\tff.Fill(&api.UserAgent)\n") - if d.RequiresLogin { - fmt.Fprint(sb, "\tff.Fill(&api.Token)\n") - } - - fmt.Fprint(sb, "\t// issue request\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// compare our response and server's one\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// check whether headers are OK\n") - fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif handler.userAgent != api.UserAgent {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// check whether the method is OK\n") - fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method) - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n") - fmt.Fprint(sb, "\t}\n") - - if d.Method == "POST" { - fmt.Fprint(sb, "\t// check the body\n") - fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct()) - fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - } else { - fmt.Fprint(sb, "\t// check the query\n") - fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - } - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestResponseLiteralNull(sb *strings.Builder) { - switch d.ResponseTypeKind() { - case reflect.Map: - // fallthrough - case reflect.Struct: - return // test not applicable - } - fmt.Fprintf(sb, "func Test%sResponseLiteralNull(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n") - fmt.Fprint(sb, "\t\tStatusCode: 200,\n") - fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`null`)},\n") - fmt.Fprint(sb, "\t}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrJSONLiteralNull) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestMandatoryFields(sb *strings.Builder) { - fields := d.StructFieldsWithTag(d.Request, tagForRequired) - if len(fields) < 1 { - return // nothing to test - } - fmt.Fprintf(sb, "func Test%sMandatoryFields(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n") - fmt.Fprint(sb, "\t\tStatusCode: 500,\n") - fmt.Fprint(sb, "\t}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprintf(sb, "\treq := &%s{} // deliberately empty\n", d.RequestTypeNameAsStruct()) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrEmptyField) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestTemplateErr(sb *strings.Builder) { - if !d.URLPath.IsTemplate { - return // nothing to test - } - fmt.Fprintf(sb, "func Test%sTemplateErr(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n") - fmt.Fprint(sb, "\t\tStatusCode: 500,\n") - fmt.Fprint(sb, "\t}}\n") - fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n") - if d.RequiresLogin == true { - fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n") - } - fmt.Fprint(sb, "\t\tTemplateExecutor: &FakeTemplateExecutor{Err: errMocked},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - d.genTestNewRequest(sb) - fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -// TODO(bassosimone): we should add a panic for every switch for -// the type of a request or a response for robustness. - -func (d *Descriptor) genAPITests(sb *strings.Builder) { - d.genTestInvalidURL(sb) - d.genTestWithMissingToken(sb) - d.genTestWithHTTPErr(sb) - d.genTestMarshalErr(sb) - d.genTestWithNewRequestErr(sb) - d.genTestWith401(sb) - d.genTestWith400(sb) - d.genTestWithResponseBodyReadErr(sb) - d.genTestWithUnmarshalFailure(sb) - d.genTestRoundTrip(sb) - d.genTestResponseLiteralNull(sb) - d.genTestMandatoryFields(sb) - d.genTestTemplateErr(sb) -} - -// GenAPIsTestGo generates apis_test.go. -func GenAPIsTestGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"encoding/json\"\n") - fmt.Fprint(&sb, "\t\"errors\"\n") - fmt.Fprint(&sb, "\t\"net/http/httptest\"\n") - fmt.Fprint(&sb, "\t\"net/http\"\n") - fmt.Fprint(&sb, "\t\"net/url\"\n") - fmt.Fprint(&sb, "\t\"strings\"\n") - fmt.Fprint(&sb, "\t\"testing\"\n") - fmt.Fprint(&sb, "\t\"sync\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - desc.genAPITests(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/caching.go b/internal/ooapi/internal/generator/caching.go deleted file mode 100644 index 1ceff42..0000000 --- a/internal/ooapi/internal/generator/caching.go +++ /dev/null @@ -1,130 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genNewCache(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s implements caching for %s.\n", - d.WithCacheAPIStructName(), d.APIStructName()) - fmt.Fprintf(sb, "type %s struct {\n", d.WithCacheAPIStructName()) - fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.CallerInterfaceName()) - fmt.Fprint(sb, "\tGobCodec GobCodec // optional\n") - fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "type %s struct {\n", d.CacheEntryName()) - fmt.Fprintf(sb, "\tReq %s\n", d.RequestTypeName()) - fmt.Fprintf(sb, "\tResp %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "// Call calls the API and implements caching.\n") - fmt.Fprintf(sb, "func (c *%s) Call(ctx context.Context, req %s) (%s, error) {\n", - d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - if d.CachePolicy == CacheAlways { - fmt.Fprint(sb, "\tif resp, _ := c.readcache(req); resp != nil {\n") - fmt.Fprint(sb, "\t\treturn resp, nil\n") - fmt.Fprint(sb, "\t}\n") - } - fmt.Fprint(sb, "\tresp, err := c.API.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - if d.CachePolicy == CacheFallback { - fmt.Fprint(sb, "\t\tif resp, _ := c.readcache(req); resp != nil {\n") - fmt.Fprint(sb, "\t\t\treturn resp, nil\n") - fmt.Fprint(sb, "\t\t}\n") - } - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif err := c.writecache(req, resp); err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn resp, nil\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (c *%s) gobCodec() GobCodec {\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\tif c.GobCodec != nil {\n") - fmt.Fprint(sb, "\t\treturn c.GobCodec\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &defaultGobCodec{}\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (c *%s) getcache() ([]%s, error) {\n", - d.WithCacheAPIStructName(), d.CacheEntryName()) - fmt.Fprintf(sb, "\tdata, err := c.KVStore.Get(\"%s\")\n", d.CacheKey()) - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar out []%s\n", d.CacheEntryName()) - fmt.Fprint(sb, "\tif err := c.gobCodec().Decode(data, &out); err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn out, nil\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (c *%s) setcache(in []%s) error {\n", - d.WithCacheAPIStructName(), d.CacheEntryName()) - fmt.Fprint(sb, "\tdata, err := c.gobCodec().Encode(in)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\treturn c.KVStore.Set(\"%s\", data)\n", d.CacheKey()) - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (c *%s) readcache(req %s) (%s, error) {\n", - d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "\tcache, err := c.getcache()\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tfor _, cur := range cache {\n") - fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n") - fmt.Fprint(sb, "\t\t\treturn cur.Resp, nil\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn nil, errCacheNotFound\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (c *%s) writecache(req %s, resp %s) error {\n", - d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "\tcache, _ := c.getcache()\n") - fmt.Fprintf(sb, "\tout := []%s{{Req: req, Resp: resp}}\n", d.CacheEntryName()) - fmt.Fprint(sb, "\tconst toomany = 64\n") - fmt.Fprint(sb, "\tfor idx, cur := range cache {\n") - fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n") - fmt.Fprint(sb, "\t\t\tcontinue // we already updated the cache\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif idx > toomany {\n") - fmt.Fprint(sb, "\t\t\tbreak\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tout = append(out, cur)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn c.setcache(out)\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(), - d.WithCacheAPIStructName()) -} - -// GenCachingGo generates caching.go. -func GenCachingGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"reflect\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - if desc.CachePolicy == CacheNone { - continue - } - desc.genNewCache(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/cachingtest.go b/internal/ooapi/internal/generator/cachingtest.go deleted file mode 100644 index fcccdba..0000000 --- a/internal/ooapi/internal/generator/cachingtest.go +++ /dev/null @@ -1,275 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genTestCacheSuccess(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestCache%sSuccess(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWriteCacheError(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestCache%sWriteCacheError(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tKVStore: &FakeKVStore{SetError: errMocked},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestFailureWithNoCache(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestCache%sFailureWithNoCache(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\tErr: errMocked,\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestFailureWithPreviousCache(sb *strings.Builder) { - // This works for both caching policies. - fmt.Fprintf(sb, "func TestCache%sFailureWithPreviousCache(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - fmt.Fprintf(sb, "\tfakeapi := &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\t\tAPI: fakeapi,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\t// first pass with no error at all\n") - fmt.Fprint(sb, "\t// use a separate scope to be sure we avoid mistakes\n") - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := cache.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t// second pass with failure\n") - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tfakeapi.Err = errMocked\n") - fmt.Fprint(sb, "\tfakeapi.Response = nil\n") - fmt.Fprint(sb, "\tresp2, err := cache.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp2 == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp2); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestSetcacheWithEncodeError(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestCache%sSetcacheWithEncodeError(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprintf(sb, "\tvar in []%s\n", d.CacheEntryName()) - fmt.Fprint(sb, "\tff.Fill(&in)\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\t\tGobCodec: &FakeCodec{EncodeErr: errMocked},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\terr := cache.setcache(in)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestReadCacheNotFound(sb *strings.Builder) { - if fields := d.StructFields(d.Request); len(fields) <= 0 { - // this test cannot work when there are no fields in the - // request because we will always find a match. - // TODO(bassosimone): how to avoid having uncovered code? - return - } - fmt.Fprintf(sb, "func TestCache%sReadCacheNotFound(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar incache []%s\n", d.CacheEntryName()) - fmt.Fprint(sb, "\tff.Fill(&incache)\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\terr := cache.setcache(incache)\n") - fmt.Fprintf(sb, "\tif err != nil {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errCacheNotFound) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif out != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil here\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWriteCacheDuplicate(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestCache%sWriteCacheDuplicate(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprintf(sb, "\tvar resp1 %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&resp1)\n") - fmt.Fprintf(sb, "\tvar resp2 %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&resp2)\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\terr := cache.writecache(req, resp1)\n") - fmt.Fprintf(sb, "\tif err != nil {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\terr = cache.writecache(req, resp2)\n") - fmt.Fprintf(sb, "\tif err != nil {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif out == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil here\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(resp2, out); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestCachSizeLimited(sb *strings.Builder) { - if fields := d.StructFields(d.Request); len(fields) <= 0 { - // this test cannot work when there are no fields in the - // request because we will always find a match. - // TODO(bassosimone): how to avoid having uncovered code? - return - } - fmt.Fprintf(sb, "func TestCache%sCacheSizeLimited(t *testing.T) {\n", d.APIStructName()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tvar prev int\n") - fmt.Fprintf(sb, "\tfor {\n") - fmt.Fprintf(sb, "\t\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\t\tff.Fill(&req)\n") - fmt.Fprintf(sb, "\t\tvar resp %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\t\tff.Fill(&resp)\n") - fmt.Fprintf(sb, "\t\terr := cache.writecache(req, resp)\n") - fmt.Fprintf(sb, "\t\tif err != nil {\n") - fmt.Fprintf(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t\t}\n") - fmt.Fprintf(sb, "\t\tout, err := cache.getcache()\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif len(out) > prev {\n") - fmt.Fprint(sb, "\t\t\tprev = len(out)\n") - fmt.Fprint(sb, "\t\t\tcontinue\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tbreak\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "}\n\n") -} - -// GenCachingTestGo generates caching_test.go. -func GenCachingTestGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"errors\"\n") - fmt.Fprint(&sb, "\t\"testing\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - if desc.CachePolicy == CacheNone { - continue - } - desc.genTestCacheSuccess(&sb) - desc.genTestWriteCacheError(&sb) - desc.genTestFailureWithNoCache(&sb) - desc.genTestFailureWithPreviousCache(&sb) - desc.genTestSetcacheWithEncodeError(&sb) - desc.genTestReadCacheNotFound(&sb) - desc.genTestWriteCacheDuplicate(&sb) - desc.genTestCachSizeLimited(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/callers.go b/internal/ooapi/internal/generator/callers.go deleted file mode 100644 index 1ce873b..0000000 --- a/internal/ooapi/internal/generator/callers.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genNewCaller(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s represents any type exposing a method\n", - d.CallerInterfaceName()) - fmt.Fprintf(sb, "// like %s.Call.\n", d.APIStructName()) - fmt.Fprintf(sb, "type %s interface {\n", d.CallerInterfaceName()) - fmt.Fprintf(sb, "\tCall(ctx context.Context, req %s) (%s, error)\n", - d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "}\n\n") -} - -// GenCallersGo generates callers.go. -func GenCallersGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - desc.genNewCaller(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/clientcall.go b/internal/ooapi/internal/generator/clientcall.go deleted file mode 100644 index 3b36063..0000000 --- a/internal/ooapi/internal/generator/clientcall.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) clientMakeAPIBase(sb *strings.Builder) { - fmt.Fprintf(sb, "&%s{\n", d.APIStructName()) - for _, field := range apiFields { - if field.ifLogin || field.ifTemplate { - continue - } - fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name) - } - fmt.Fprint(sb, "}") -} - -func (d *Descriptor) clientMakeAPI(sb *strings.Builder) { - if d.RequiresLogin && d.CachePolicy != CacheNone { - panic("we don't support requiresLogin with caching") - } - if d.RequiresLogin { - fmt.Fprintf(sb, "&%s{\n", d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tAPI:") - d.clientMakeAPIBase(sb) - fmt.Fprint(sb, ",\n") - fmt.Fprint(sb, "\tJSONCodec: c.JSONCodec,\n") - fmt.Fprint(sb, "\tKVStore: c.KVStore,\n") - fmt.Fprint(sb, "\tRegisterAPI: &simpleRegisterAPI{\n") - for _, field := range apiFields { - if field.ifLogin || field.ifTemplate { - continue - } - fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name) - } - fmt.Fprint(sb, "\t},\n") - fmt.Fprint(sb, "\tLoginAPI: &simpleLoginAPI{\n") - for _, field := range apiFields { - if field.ifLogin || field.ifTemplate { - continue - } - fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name) - } - fmt.Fprint(sb, "\t},\n") - fmt.Fprint(sb, "}\n") - return - } - if d.CachePolicy != CacheNone { - fmt.Fprintf(sb, "&%s{\n", d.WithCacheAPIStructName()) - fmt.Fprint(sb, "\tAPI:") - d.clientMakeAPIBase(sb) - fmt.Fprint(sb, ",\n") - fmt.Fprint(sb, "\tGobCodec: c.GobCodec,\n") - fmt.Fprint(sb, "\tKVStore: c.KVStore,\n") - fmt.Fprint(sb, "}\n") - return - } - d.clientMakeAPIBase(sb) - fmt.Fprint(sb, "\n") -} - -func (d *Descriptor) genClientNewCaller(sb *strings.Builder) { - fmt.Fprintf(sb, "func (c *Client) new%sCaller() ", d.Name) - fmt.Fprintf(sb, "%s {\n", d.CallerInterfaceName()) - fmt.Fprint(sb, "\treturn ") - d.clientMakeAPI(sb) - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genClientCall(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s calls the %s API.\n", d.Name, d.Name) - fmt.Fprintf(sb, "func (c *Client) %s(\n", d.Name) - fmt.Fprintf(sb, "ctx context.Context, req %s,\n) ", d.RequestTypeName()) - fmt.Fprintf(sb, "(%s, error) {\n", d.ResponseTypeName()) - fmt.Fprintf(sb, "\tapi := c.new%sCaller()\n", d.Name) - fmt.Fprint(sb, "\treturn api.Call(ctx, req)\n") - fmt.Fprint(sb, "}\n\n") -} - -// GenClientCallGo generates clientcall.go. -func GenClientCallGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - switch desc.Name { - case "Register", "Login": - // We don't want to generate these APIs as toplevel. - continue - } - desc.genClientNewCaller(&sb) - desc.genClientCall(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/clientcalltest.go b/internal/ooapi/internal/generator/clientcalltest.go deleted file mode 100644 index b1f8154..0000000 --- a/internal/ooapi/internal/generator/clientcalltest.go +++ /dev/null @@ -1,182 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genTestClientCallRoundTrip(sb *strings.Builder) { - // generate the type of the handler - fmt.Fprintf(sb, "type handleClientCall%s struct {\n", d.Name) - fmt.Fprint(sb, "\taccept string\n") - fmt.Fprint(sb, "\tbody []byte\n") - fmt.Fprint(sb, "\tcontentType string\n") - fmt.Fprint(sb, "\tcount int32\n") - fmt.Fprint(sb, "\tmethod string\n") - fmt.Fprint(sb, "\tmu sync.Mutex\n") - fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\turl *url.URL\n") - fmt.Fprint(sb, "\tuserAgent string\n") - fmt.Fprint(sb, "}\n\n") - - // generate the handling function - fmt.Fprintf(sb, - "func (h *handleClientCall%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {", - d.Name) - fmt.Fprint(sb, "\tff := fakeFill{}\n") - if d.RequiresLogin { - fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/register\" {\n") - fmt.Fprintf(sb, "\t\tvar out apimodel.RegisterResponse\n") - fmt.Fprintf(sb, "\t\tff.Fill(&out)\n") - fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n") - fmt.Fprintf(sb, "\t\tif err != nil {\n") - fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\t\treturn\n") - fmt.Fprintf(sb, "\t\t}\n") - fmt.Fprintf(sb, "\t\tw.Write(data)\n") - fmt.Fprintf(sb, "\t\treturn\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/login\" {\n") - fmt.Fprintf(sb, "\t\tvar out apimodel.LoginResponse\n") - fmt.Fprintf(sb, "\t\tff.Fill(&out)\n") - fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n") - fmt.Fprintf(sb, "\t\tif err != nil {\n") - fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\t\treturn\n") - fmt.Fprintf(sb, "\t\t}\n") - fmt.Fprintf(sb, "\t\tw.Write(data)\n") - fmt.Fprintf(sb, "\t\treturn\n") - fmt.Fprintf(sb, "\t}\n") - } - fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n") - fmt.Fprint(sb, "\th.mu.Lock()\n") - fmt.Fprint(sb, "\tif h.count > 0 {\n") - fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n") - fmt.Fprint(sb, "\t\treturn\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\th.count++\n") - fmt.Fprint(sb, "\tif r.Body != nil {\n") - fmt.Fprint(sb, "\t\tdata, err := netxlite.ReadAllContext(r.Context(), r.Body)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\t\treturn\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\th.body = data\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\th.method = r.Method\n") - fmt.Fprint(sb, "\th.url = r.URL\n") - fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n") - fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n") - fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n") - fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&out)\n") - fmt.Fprintf(sb, "\th.resp = out\n") - fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n") - fmt.Fprintf(sb, "\tif err != nil {\n") - fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n") - fmt.Fprintf(sb, "\t\treturn\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tw.Write(data)\n") - fmt.Fprintf(sb, "\t}\n\n") - - // generate the test itself - fmt.Fprintf(sb, "func Test%sClientCallRoundTrip(t *testing.T) {\n", d.Name) - - fmt.Fprint(sb, "\t// setup\n") - fmt.Fprintf(sb, "\thandler := &handleClientCall%s{}\n", d.Name) - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct()) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tclnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}\n") - fmt.Fprint(sb, "\tff.Fill(&clnt.UserAgent)\n") - - fmt.Fprint(sb, "\t// issue request\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprintf(sb, "\tresp, err := clnt.%s(ctx, req)\n", d.Name) - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// compare our response and server's one\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// check whether headers are OK\n") - fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif handler.userAgent != clnt.UserAgent {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// check whether the method is OK\n") - fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method) - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n") - fmt.Fprint(sb, "\t}\n") - - if d.Method == "POST" { - fmt.Fprint(sb, "\t// check the body\n") - fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct()) - fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - } else { - fmt.Fprint(sb, "\t// check the query\n") - fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName()) - fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - } - - fmt.Fprint(sb, "}\n\n") -} - -// GenClientCallTestGo generates clientcall_test.go. -func GenClientCallTestGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"encoding/json\"\n") - fmt.Fprint(&sb, "\t\"net/http/httptest\"\n") - fmt.Fprint(&sb, "\t\"net/http\"\n") - fmt.Fprint(&sb, "\t\"net/url\"\n") - fmt.Fprint(&sb, "\t\"testing\"\n") - fmt.Fprint(&sb, "\t\"sync\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - if desc.Name == "Login" || desc.Name == "Register" { - continue // they cannot be called directly - } - desc.genTestClientCallRoundTrip(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/cloners.go b/internal/ooapi/internal/generator/cloners.go deleted file mode 100644 index f271c42..0000000 --- a/internal/ooapi/internal/generator/cloners.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genNewCloner(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s represents any type exposing a method\n", - d.ClonerInterfaceName()) - fmt.Fprintf(sb, "// like %s.WithToken.\n", d.APIStructName()) - fmt.Fprintf(sb, "type %s interface {\n", d.ClonerInterfaceName()) - fmt.Fprintf(sb, "\tWithToken(token string) %s\n", d.CallerInterfaceName()) - fmt.Fprint(sb, "}\n\n") -} - -// GenClonersGo generates cloners.go. -func GenClonersGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - for _, desc := range Descriptors { - if !desc.RequiresLogin { - continue - } - desc.genNewCloner(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/fakeapitest.go b/internal/ooapi/internal/generator/fakeapitest.go deleted file mode 100644 index fa40441..0000000 --- a/internal/ooapi/internal/generator/fakeapitest.go +++ /dev/null @@ -1,61 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genNewFakeAPI(sb *strings.Builder) { - fmt.Fprintf(sb, "type %s struct {\n", d.FakeAPIStructName()) - if d.RequiresLogin { - fmt.Fprintf(sb, "\tWithResult %s\n", d.CallerInterfaceName()) - } - fmt.Fprint(sb, "\tErr error\n") - fmt.Fprintf(sb, "\tResponse %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tCountCall *atomicx.Int64\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (fapi *%s) Call(ctx context.Context, req %s) (%s, error) {\n", - d.FakeAPIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "\tif fapi.CountCall != nil {\n") - fmt.Fprint(sb, "\t\tfapi.CountCall.Add(1)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn fapi.Response, fapi.Err\n") - fmt.Fprint(sb, "}\n\n") - - if d.RequiresLogin { - fmt.Fprintf(sb, "func (fapi *%s) WithToken(token string) %s {\n", - d.FakeAPIStructName(), d.CallerInterfaceName()) - fmt.Fprint(sb, "\treturn fapi.WithResult\n") - fmt.Fprint(sb, "}\n\n") - } - - fmt.Fprint(sb, "var (\n") - fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.CallerInterfaceName(), - d.FakeAPIStructName()) - if d.RequiresLogin { - fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.ClonerInterfaceName(), - d.FakeAPIStructName()) - } - fmt.Fprint(sb, ")\n\n") -} - -// GenFakeAPITestGo generates fakeapi_test.go. -func GenFakeAPITestGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - desc.genNewFakeAPI(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/generator.go b/internal/ooapi/internal/generator/generator.go deleted file mode 100644 index be621ad..0000000 --- a/internal/ooapi/internal/generator/generator.go +++ /dev/null @@ -1,57 +0,0 @@ -// Command generator generates code in the ooapi package. -// -// To this end, it uses the content of the apimodel package as -// well as the content of the spec.go file. -// -// The apimodel package defines the model, i.e., the structure -// of requests and responses and how messages should be sent -// and received. -// -// The spec.go file describes all the implemented APIs. -// -// If you change apimodel or spec.go, remember to run the -// `go generate ./...` command to regenerate all files. -package main - -import ( - "flag" - "fmt" -) - -var flagFile = flag.String("file", "", "Indicate which file to regenerate") - -func main() { - flag.Parse() - switch file := *flagFile; file { - case "apis.go": - GenAPIsGo(file) - case "responses.go": - GenResponsesGo(file) - case "requests.go": - GenRequestsGo(file) - case "swagger_test.go": - GenSwaggerTestGo(file) - case "apis_test.go": - GenAPIsTestGo(file) - case "callers.go": - GenCallersGo(file) - case "caching.go": - GenCachingGo(file) - case "login.go": - GenLoginGo(file) - case "cloners.go": - GenClonersGo(file) - case "fakeapi_test.go": - GenFakeAPITestGo(file) - case "caching_test.go": - GenCachingTestGo(file) - case "login_test.go": - GenLoginTestGo(file) - case "clientcall.go": - GenClientCallGo(file) - case "clientcall_test.go": - GenClientCallTestGo(file) - default: - panic(fmt.Sprintf("don't know how to create this file: %s", file)) - } -} diff --git a/internal/ooapi/internal/generator/login.go b/internal/ooapi/internal/generator/login.go deleted file mode 100644 index c58ee3d..0000000 --- a/internal/ooapi/internal/generator/login.go +++ /dev/null @@ -1,182 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genNewLogin(sb *strings.Builder) { - fmt.Fprintf(sb, "// %s implements login for %s.\n", - d.WithLoginAPIStructName(), d.APIStructName()) - fmt.Fprintf(sb, "type %s struct {\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.ClonerInterfaceName()) - fmt.Fprint(sb, "\tJSONCodec JSONCodec // optional\n") - fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n") - fmt.Fprint(sb, "\tRegisterAPI callerForRegisterAPI // mandatory\n") - fmt.Fprint(sb, "\tLoginAPI callerForLoginAPI // mandatory\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "// Call logins, if needed, then calls the API.\n") - fmt.Fprintf(sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n", - d.WithLoginAPIStructName(), d.RequestTypeName(), d.ResponseTypeName()) - fmt.Fprint(sb, "\ttoken, err := api.maybeLogin(ctx)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tresp, err := api.API.WithToken(token).Call(ctx, req)\n") - fmt.Fprint(sb, "\tif errors.Is(err, ErrUnauthorized) {\n") - fmt.Fprint(sb, "\t\t// Maybe the clock is just off? Let's try to obtain\n") - fmt.Fprint(sb, "\t\t// a token again and see if this fixes it.\n") - fmt.Fprint(sb, "\t\tif token, err = api.forceLogin(ctx); err == nil {\n") - fmt.Fprint(sb, "\t\t\tswitch resp, err = api.API.WithToken(token).Call(ctx, req); err {\n") - fmt.Fprint(sb, "\t\t\tcase nil:\n") - fmt.Fprint(sb, "\t\t\t\treturn resp, nil\n") - fmt.Fprint(sb, "\t\t\tcase ErrUnauthorized:\n") - fmt.Fprint(sb, "\t\t\t\t// fallthrough\n") - fmt.Fprint(sb, "\t\t\tdefault:\n") - fmt.Fprint(sb, "\t\t\t\treturn nil, err\n") - fmt.Fprint(sb, "\t\t\t}\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\t// Okay, this seems a broader problem. How about we try\n") - fmt.Fprint(sb, "\t\t// and re-register ourselves again instead?\n") - fmt.Fprint(sb, "\t\ttoken, err = api.forceRegister(ctx)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\treturn nil, err\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tresp, err = api.API.WithToken(token).Call(ctx, req)\n") - fmt.Fprint(sb, "\t\t// fallthrough\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn resp, nil\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n") - fmt.Fprint(sb, "\t\treturn api.JSONCodec\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) readstate() (*loginState, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tdata, err := api.KVStore.Get(loginKey)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tvar ls loginState\n") - fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &ls); err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn &ls, nil\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) writestate(ls *loginState) error {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tdata, err := api.jsonCodec().Encode(*ls)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn api.KVStore.Set(loginKey, data)\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) doRegister(ctx context.Context, password string) (string, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\treq := newRegisterRequest(password)\n") - fmt.Fprint(sb, "\tls := &loginState{}\n") - fmt.Fprint(sb, "\tresp, err := api.RegisterAPI.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn \"\", err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tls.ClientID = resp.ClientID\n") - fmt.Fprint(sb, "\tls.Password = req.Password\n") - fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) forceRegister(ctx context.Context) (string, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tvar password string\n") - fmt.Fprint(sb, "\t// If we already have a previous password, let us keep\n") - fmt.Fprint(sb, "\t// using it. This will allow a new version of the API to\n") - fmt.Fprint(sb, "\t// be able to continue to identify this probe. (This\n") - fmt.Fprint(sb, "\t// assumes that we have a stateless API that generates\n") - fmt.Fprint(sb, "\t// the user ID as a signature of the password plus a\n") - fmt.Fprint(sb, "\t// timestamp and that the key to generate the signature\n") - fmt.Fprint(sb, "\t// is not lost. If all these conditions are met, we\n") - fmt.Fprint(sb, "\t// can then serve better test targets to more long running\n") - fmt.Fprint(sb, "\t// (and therefore trusted) probes.)\n") - fmt.Fprint(sb, "\tif ls, err := api.readstate(); err == nil {\n") - fmt.Fprint(sb, "\t\tpassword = ls.Password\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif password == \"\" {\n") - fmt.Fprint(sb, "\t\tpassword = newRandomPassword()\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn api.doRegister(ctx, password)\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) forceLogin(ctx context.Context) (string, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tls, err := api.readstate()\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn \"\", err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) maybeLogin(ctx context.Context) (string, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\tls, _ := api.readstate()\n") - fmt.Fprint(sb, "\tif ls == nil || !ls.credentialsValid() {\n") - fmt.Fprint(sb, "\t\treturn api.forceRegister(ctx)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif !ls.tokenValid() {\n") - fmt.Fprint(sb, "\t\treturn api.doLogin(ctx, ls)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn ls.Token, nil\n") - fmt.Fprint(sb, "}\n\n") - - fmt.Fprintf(sb, "func (api *%s) doLogin(ctx context.Context, ls *loginState) (string, error) {\n", - d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\treq := &apimodel.LoginRequest{\n") - fmt.Fprint(sb, "\t\tClientID: ls.ClientID,\n") - fmt.Fprint(sb, "\t\tPassword: ls.Password,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tresp, err := api.LoginAPI.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn \"\", err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tls.Token = resp.Token\n") - fmt.Fprint(sb, "\tls.Expire = resp.Expire\n") - fmt.Fprint(sb, "\tif err := api.writestate(ls); err != nil {\n") - fmt.Fprint(sb, "\t\treturn \"\", err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\treturn ls.Token, nil\n") - fmt.Fprint(sb, "}\n\n") - fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(), - d.WithLoginAPIStructName()) -} - -// GenLoginGo generates login.go. -func GenLoginGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"errors\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - if !desc.RequiresLogin { - continue - } - desc.genNewLogin(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/logintest.go b/internal/ooapi/internal/generator/logintest.go deleted file mode 100644 index 9bfa75a..0000000 --- a/internal/ooapi/internal/generator/logintest.go +++ /dev/null @@ -1,899 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "time" -) - -func (d *Descriptor) genTestRegisterAndLoginSuccess(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestRegisterAndLogin%sSuccess(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n") - fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n") - fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n") - fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestContinueUsingToken(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sContinueUsingToken(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n") - fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n") - fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n") - fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we disable register and login but we\n") - fmt.Fprint(sb, "\t// should be okay because of the token\n") - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tregisterAPI.Err = errMocked\n") - fmt.Fprint(sb, "\tregisterAPI.Response = nil\n") - fmt.Fprint(sb, "\tloginAPI.Err = errMocked\n") - fmt.Fprint(sb, "\tloginAPI.Response = nil\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithValidButExpiredToken(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithValidButExpiredToken(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tErr: errMocked,\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n") - fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n") - fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tls := &loginState{\n") - fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n") - fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n") - fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n") - fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n") - fmt.Fprint(sb, "\t\tt.Fatal(diff)\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 0 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithRegisterAPIError(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithRegisterAPIError(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tErr: errMocked,\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestWithLoginFailure(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sWithLoginFailure(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n") - fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tErr: errMocked,\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestRegisterAndLoginThenFail(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestRegisterAndLogin%sThenFail(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n") - fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n") - fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n") - fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tErr: errMocked,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestTheDatabaseIsReplaced(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplaced(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\thandler := &LoginHandler{\n") - fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tt: t,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - - fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI : baseAPI,\n") - fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n") - fmt.Fprint(sb, "\thandler.forgetLogins()\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestTheDatabaseIsReplacedThenFailure(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplacedThenFailure(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\thandler := &LoginHandler{\n") - fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tt: t,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - - fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI : baseAPI,\n") - fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n") - fmt.Fprint(sb, "\t// but registrations are also failing.\n") - fmt.Fprint(sb, "\thandler.forgetLogins()\n") - fmt.Fprint(sb, "\thandler.noRegister = true\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestRegisterAndLoginCannotWriteState(sb *strings.Builder) { - fmt.Fprintf(sb, "func TestRegisterAndLogin%sCannotWriteState(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n") - fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n") - fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n") - fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n") - fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n") - fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName()) - fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName()) - fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n") - fmt.Fprint(sb, "\t\t\t},\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{\n") - fmt.Fprint(sb, "\t\t\tEncodeErr: errMocked,\n") - fmt.Fprint(sb, "\t\t},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestReadStateDecodeFailure(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sReadStateDecodeFailure(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName()) - fmt.Fprint(sb, "\tff.Fill(&expect)\n") - - fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tls := &loginState{\n") - fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n") - fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n") - fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n") - fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(err)\n") - fmt.Fprintf(sb, "\t}\n") - - fmt.Fprintf(sb, "\tout, err := login.forceLogin(context.Background())\n") - fmt.Fprintf(sb, "if !errors.Is(err, errMocked) {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprintf(sb, "\t}\n") - fmt.Fprintf(sb, "if out != \"\" {\n") - fmt.Fprintf(sb, "\t\tt.Fatal(\"expected empty string here\")\n") - fmt.Fprintf(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestClockIsOffThenSuccess(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sClockIsOffThenSuccess(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\thandler := &LoginHandler{\n") - fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tt: t,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - - fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI : baseAPI,\n") - fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n") - fmt.Fprint(sb, "\t// this should simulate the client clock\n") - fmt.Fprint(sb, "\t// being off and considering a token still valid\n") - fmt.Fprint(sb, "\thandler.forgetTokens()\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestClockIsOffThen401(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sClockIsOffThen401(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\thandler := &LoginHandler{\n") - fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tt: t,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - - fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI : baseAPI,\n") - fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n") - fmt.Fprint(sb, "\t// this should simulate the client clock\n") - fmt.Fprint(sb, "\t// being off and considering a token still valid\n") - fmt.Fprint(sb, "\thandler.forgetTokens()\n") - fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 401}\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp == nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -func (d *Descriptor) genTestClockIsOffThen500(sb *strings.Builder) { - fmt.Fprintf(sb, "func Test%sClockIsOffThen500(t *testing.T) {\n", d.Name) - fmt.Fprint(sb, "\tff := &fakeFill{}\n") - fmt.Fprint(sb, "\thandler := &LoginHandler{\n") - fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n") - fmt.Fprint(sb, "\t\tt: t,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n") - fmt.Fprint(sb, "\tdefer srvr.Close()\n") - - fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n") - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName()) - fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n") - fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName()) - fmt.Fprintf(sb, "\tAPI : baseAPI,\n") - fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n") - fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n") - fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName()) - fmt.Fprint(sb, "\tff.Fill(&req)\n") - fmt.Fprint(sb, "\tctx := context.Background()\n") - - fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n") - fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n") - - fmt.Fprint(sb, "\t{\n") - fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\t\tif err != nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t\tif resp == nil {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t\t}\n") - - fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t\t}\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n") - fmt.Fprint(sb, "\t// this should simulate the client clock\n") - fmt.Fprint(sb, "\t// being off and considering a token still valid\n") - fmt.Fprint(sb, "\thandler.forgetTokens()\n") - fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 500}\n") - - fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n") - fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp != nil {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n") - fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n") - fmt.Fprint(sb, "\t}\n") - - fmt.Fprint(sb, "}\n\n") -} - -// GenLoginTestGo generates login_test.go. -func GenLoginTestGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"errors\"\n") - fmt.Fprint(&sb, "\t\"net/http/httptest\"\n") - fmt.Fprint(&sb, "\t\"testing\"\n") - fmt.Fprint(&sb, "\t\"time\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n") - for _, desc := range Descriptors { - if !desc.RequiresLogin { - continue - } - desc.genTestRegisterAndLoginSuccess(&sb) - desc.genTestContinueUsingToken(&sb) - desc.genTestWithValidButExpiredToken(&sb) - desc.genTestWithRegisterAPIError(&sb) - desc.genTestWithLoginFailure(&sb) - desc.genTestRegisterAndLoginThenFail(&sb) - desc.genTestTheDatabaseIsReplaced(&sb) - desc.genTestRegisterAndLoginCannotWriteState(&sb) - desc.genTestReadStateDecodeFailure(&sb) - desc.genTestTheDatabaseIsReplacedThenFailure(&sb) - desc.genTestClockIsOffThenSuccess(&sb) - desc.genTestClockIsOffThen401(&sb) - desc.genTestClockIsOffThen500(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/reflect.go b/internal/ooapi/internal/generator/reflect.go deleted file mode 100644 index 767dbce..0000000 --- a/internal/ooapi/internal/generator/reflect.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "fmt" - "reflect" -) - -// TypeName returns v's package-qualified type name. -func (d *Descriptor) TypeName(v interface{}) string { - return reflect.TypeOf(v).String() -} - -// RequestTypeName calls d.TypeName(d.Request). -func (d *Descriptor) RequestTypeName() string { - return d.TypeName(d.Request) -} - -// ResponseTypeName calls d.TypeName(d.Response). -func (d *Descriptor) ResponseTypeName() string { - return d.TypeName(d.Response) -} - -// APIStructName returns the correct struct type name -// for the API we're currently processing. -func (d *Descriptor) APIStructName() string { - return fmt.Sprintf("simple%sAPI", d.Name) -} - -// FakeAPIStructName returns the correct struct type name -// for the fake for the API we're currently processing. -func (d *Descriptor) FakeAPIStructName() string { - return fmt.Sprintf("Fake%sAPI", d.Name) -} - -// WithLoginAPIStructName returns the correct struct type name -// for the WithLoginAPI we're currently processing. -func (d *Descriptor) WithLoginAPIStructName() string { - return fmt.Sprintf("withLogin%sAPI", d.Name) -} - -// CallerInterfaceName returns the correct caller interface name -// for the API we're currently processing. -func (d *Descriptor) CallerInterfaceName() string { - return fmt.Sprintf("callerFor%sAPI", d.Name) -} - -// ClonerInterfaceName returns the correct cloner interface name -// for the API we're currently processing. -func (d *Descriptor) ClonerInterfaceName() string { - return fmt.Sprintf("clonerFor%sAPI", d.Name) -} - -// WithCacheAPIStructName returns the correct struct type name for -// the cache for the API we're currently processing. -func (d *Descriptor) WithCacheAPIStructName() string { - return fmt.Sprintf("withCache%sAPI", d.Name) -} - -// CacheEntryName returns the correct struct type name for the -// cache entry for the API we're currently processing. -func (d *Descriptor) CacheEntryName() string { - return fmt.Sprintf("cacheEntryFor%sAPI", d.Name) -} - -// CacheKey returns the correct cache key for the API -// we're currently processing. -func (d *Descriptor) CacheKey() string { - return fmt.Sprintf("%s.cache", d.Name) -} - -// StructFields returns all the struct fields of in. This function -// assumes that in is a pointer to struct, and will otherwise panic. -func (d *Descriptor) StructFields(in interface{}) []*reflect.StructField { - t := reflect.TypeOf(in) - if t.Kind() != reflect.Ptr { - panic("not a pointer") - } - t = t.Elem() - if t.Kind() != reflect.Struct { - panic("not a struct") - } - var out []*reflect.StructField - for idx := 0; idx < t.NumField(); idx++ { - f := t.Field(idx) - out = append(out, &f) - } - return out -} - -// StructFieldsWithTag returns all the struct fields of -// in that have the specified tag. -func (d *Descriptor) StructFieldsWithTag(in interface{}, tag string) []*reflect.StructField { - var out []*reflect.StructField - for _, f := range d.StructFields(in) { - if f.Tag.Get(tag) != "" { - out = append(out, f) - } - } - return out -} - -// RequestOrResponseTypeKind returns the type kind of in, which should -// be a request or a response. This function assumes that in is either a -// pointer to struct or a map and will panic otherwise. -func (d *Descriptor) RequestOrResponseTypeKind(in interface{}) reflect.Kind { - t := reflect.TypeOf(in) - if t.Kind() == reflect.Ptr { - t = t.Elem() - if t.Kind() != reflect.Struct { - panic("not a struct") - } - return reflect.Struct - } - if t.Kind() != reflect.Map { - panic("not a map") - } - return reflect.Map -} - -// RequestTypeKind calls d.RequestOrResponseTypeKind(d.Request). -func (d *Descriptor) RequestTypeKind() reflect.Kind { - return d.RequestOrResponseTypeKind(d.Request) -} - -// ResponseTypeKind calls d.RequestOrResponseTypeKind(d.Response). -func (d *Descriptor) ResponseTypeKind() reflect.Kind { - return d.RequestOrResponseTypeKind(d.Response) -} - -// TypeNameAsStruct assumes that in is a pointer to struct and -// returns the type of the corresponding struct. The returned -// type is package qualified. -func (d *Descriptor) TypeNameAsStruct(in interface{}) string { - t := reflect.TypeOf(in) - if t.Kind() != reflect.Ptr { - panic("not a pointer") - } - t = t.Elem() - if t.Kind() != reflect.Struct { - panic("not a struct") - } - return t.String() -} - -// RequestTypeNameAsStruct calls d.TypeNameAsStruct(d.Request) -func (d *Descriptor) RequestTypeNameAsStruct() string { - return d.TypeNameAsStruct(d.Request) -} - -// ResponseTypeNameAsStruct calls d.TypeNameAsStruct(d.Response) -func (d *Descriptor) ResponseTypeNameAsStruct() string { - return d.TypeNameAsStruct(d.Response) -} diff --git a/internal/ooapi/internal/generator/requests.go b/internal/ooapi/internal/generator/requests.go deleted file mode 100644 index 2041e21..0000000 --- a/internal/ooapi/internal/generator/requests.go +++ /dev/null @@ -1,141 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "strings" - "time" -) - -const ( - tagForQuery = "query" - tagForRequired = "required" -) - -func (d *Descriptor) genNewRequestQueryElemString(sb *strings.Builder, f *reflect.StructField) { - name := f.Name - query := f.Tag.Get(tagForQuery) - if f.Tag.Get(tagForRequired) == "true" { - fmt.Fprintf(sb, "\tif req.%s == \"\" {\n", name) - fmt.Fprintf(sb, "\t\treturn nil, newErrEmptyField(\"%s\")\n", name) - fmt.Fprint(sb, "\t}\n") - fmt.Fprintf(sb, "\tq.Add(\"%s\", req.%s)\n", query, name) - return - } - fmt.Fprintf(sb, "\tif req.%s != \"\" {\n", name) - fmt.Fprintf(sb, "\t\tq.Add(\"%s\", req.%s)\n", query, name) - fmt.Fprint(sb, "\t}\n") -} - -func (d *Descriptor) genNewRequestQueryElemBool(sb *strings.Builder, f *reflect.StructField) { - // required does not make much sense for a boolean field - name := f.Name - query := f.Tag.Get(tagForQuery) - fmt.Fprintf(sb, "\tif req.%s {\n", name) - fmt.Fprintf(sb, "\t\tq.Add(\"%s\", \"true\")\n", query) - fmt.Fprint(sb, "\t}\n") -} - -func (d *Descriptor) genNewRequestQueryElemInt64(sb *strings.Builder, f *reflect.StructField) { - // required does not make much sense for an integer field - name := f.Name - query := f.Tag.Get(tagForQuery) - fmt.Fprintf(sb, "\tif req.%s != 0 {\n", name) - fmt.Fprintf(sb, "\t\tq.Add(\"%s\", newQueryFieldInt64(req.%s))\n", query, name) - fmt.Fprint(sb, "\t}\n") -} - -func (d *Descriptor) genNewRequestQuery(sb *strings.Builder) { - if d.Method != "GET" { - return // we only generate query for GET - } - fields := d.StructFieldsWithTag(d.Request, tagForQuery) - if len(fields) <= 0 { - return - } - fmt.Fprint(sb, "\tq := url.Values{}\n") - for idx, f := range fields { - switch f.Type.Kind() { - case reflect.String: - d.genNewRequestQueryElemString(sb, f) - case reflect.Bool: - d.genNewRequestQueryElemBool(sb, f) - case reflect.Int64: - d.genNewRequestQueryElemInt64(sb, f) - default: - panic(fmt.Sprintf("unexpected query type at index %d", idx)) - } - } - fmt.Fprint(sb, "\tURL.RawQuery = q.Encode()\n") -} - -func (d *Descriptor) genNewRequestCallNewRequest(sb *strings.Builder) { - if d.Method == "POST" { - fmt.Fprint(sb, "\tbody, err := api.jsonCodec().Encode(req)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tout, err := api.requestMaker().NewRequest(") - fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method) - fmt.Fprint(sb, "bytes.NewReader(body))\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tout.Header.Set(\"Content-Type\", \"application/json\")\n") - fmt.Fprint(sb, "\treturn out, nil\n") - return - } - fmt.Fprint(sb, "\treturn api.requestMaker().NewRequest(") - fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method) - fmt.Fprint(sb, "nil)\n") -} - -func (d *Descriptor) genNewRequest(sb *strings.Builder) { - - fmt.Fprintf( - sb, "func (api *%s) newRequest(ctx context.Context, req %s) %s {\n", - d.APIStructName(), d.RequestTypeName(), "(*http.Request, error)") - fmt.Fprint(sb, "\tURL, err := url.Parse(api.baseURL())\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - - switch d.URLPath.IsTemplate { - case false: - fmt.Fprintf(sb, "\tURL.Path = \"%s\"\n", d.URLPath.Value) - case true: - fmt.Fprintf( - sb, "\tup, err := api.templateExecutor().Execute(\"%s\", req)\n", - d.URLPath.Value) - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tURL.Path = up\n") - } - - d.genNewRequestQuery(sb) - d.genNewRequestCallNewRequest(sb) - - fmt.Fprintf(sb, "}\n\n") -} - -// GenRequestsGo generates requests.go. -func GenRequestsGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"bytes\"\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"net/http\"\n") - fmt.Fprint(&sb, "\t\"net/url\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n\n") - for _, desc := range Descriptors { - desc.genNewRequest(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/responses.go b/internal/ooapi/internal/generator/responses.go deleted file mode 100644 index 821bae6..0000000 --- a/internal/ooapi/internal/generator/responses.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "fmt" - "reflect" - "strings" - "time" -) - -func (d *Descriptor) genNewResponse(sb *strings.Builder) { - fmt.Fprintf(sb, - "func (api *%s) newResponse(ctx context.Context, resp *http.Response, err error) (%s, error) {\n", - d.APIStructName(), d.ResponseTypeName()) - - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp.StatusCode == 401 {\n") - fmt.Fprint(sb, "\t\treturn nil, ErrUnauthorized\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tif resp.StatusCode != 200 {\n") - fmt.Fprint(sb, "\t\treturn nil, newHTTPFailure(resp.StatusCode)\n") - fmt.Fprint(sb, "\t}\n") - fmt.Fprint(sb, "\tdefer resp.Body.Close()\n") - fmt.Fprint(sb, "\treader := io.LimitReader(resp.Body, 4<<20)\n") - fmt.Fprint(sb, "\tdata, err := netxlite.ReadAllContext(ctx, reader)\n") - fmt.Fprint(sb, "\tif err != nil {\n") - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - - switch d.ResponseTypeKind() { - case reflect.Map: - fmt.Fprintf(sb, "\tout := %s{}\n", d.ResponseTypeName()) - case reflect.Struct: - fmt.Fprintf(sb, "\tout := &%s{}\n", d.ResponseTypeNameAsStruct()) - } - - switch d.ResponseTypeKind() { - case reflect.Map: - fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &out); err != nil {\n") - case reflect.Struct: - fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, out); err != nil {\n") - } - - fmt.Fprint(sb, "\t\treturn nil, err\n") - fmt.Fprint(sb, "\t}\n") - - switch d.ResponseTypeKind() { - case reflect.Map: - // For rationale, see https://play.golang.org/p/m9-MsTaQ5wt and - // https://play.golang.org/p/6h-v-PShMk9. - fmt.Fprint(sb, "\tif out == nil {\n") - fmt.Fprint(sb, "\t\treturn nil, ErrJSONLiteralNull\n") - fmt.Fprint(sb, "\t}\n") - case reflect.Struct: - // nothing - } - fmt.Fprintf(sb, "\treturn out, nil\n") - fmt.Fprintf(sb, "}\n\n") -} - -// GenResponsesGo generates responses.go. -func GenResponsesGo(file string) { - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprint(&sb, "import (\n") - fmt.Fprint(&sb, "\t\"context\"\n") - fmt.Fprint(&sb, "\t\"io\"\n") - fmt.Fprint(&sb, "\t\"net/http\"\n") - fmt.Fprint(&sb, "\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/netxlite\"\n") - fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n") - fmt.Fprint(&sb, ")\n\n") - for _, desc := range Descriptors { - desc.genNewResponse(&sb) - } - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/spec.go b/internal/ooapi/internal/generator/spec.go deleted file mode 100644 index a8a6cde..0000000 --- a/internal/ooapi/internal/generator/spec.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" - -// URLPath describes a URLPath. -type URLPath struct { - // IsTemplate indicates whether Value contains a template. A future - // version of this implementation will automatically deduce that. - IsTemplate bool - - // Value is the value of the URL path. - Value string - - // InSwagger indicates the corresponding name to be used in - // the Swagger specification. - InSwagger string -} - -// Descriptor is an API descriptor. It tells the generator -// what code it should emit for a given API. -type Descriptor struct { - // Name is the name of the API. - Name string - - // CachePolicy indicates the caching policy to use. - CachePolicy int - - // RequiresLogin indicates whether the API requires login. - RequiresLogin bool - - // Method is the method to use ("GET" or "POST"). - Method string - - // URLPath is the URL path. - URLPath URLPath - - // Request is an instance of the request type. - Request interface{} - - // Response is an instance of the response type. - Response interface{} -} - -// These are the caching policies. -const ( - // CacheNone indicates we don't use a cache. - CacheNone = iota - - // CacheFallback indicates we fallback to the cache - // when there is a failure. - CacheFallback - - // CacheAlways indicates that we always check the - // cache before sending a request. - CacheAlways -) - -// Descriptors describes all the APIs. -// -// Note that it matters whether the requests and responses -// are pointers. Generally speaking, if the message is a -// struct, use a pointer. If it's a map, don't. -var Descriptors = []Descriptor{{ - Name: "CheckReportID", - Method: "GET", - URLPath: URLPath{Value: "/api/_/check_report_id"}, - Request: &apimodel.CheckReportIDRequest{}, - Response: &apimodel.CheckReportIDResponse{}, -}, { - Name: "CheckIn", - Method: "POST", - URLPath: URLPath{Value: "/api/v1/check-in"}, - Request: &apimodel.CheckInRequest{}, - Response: &apimodel.CheckInResponse{}, -}, { - Name: "Login", - Method: "POST", - URLPath: URLPath{Value: "/api/v1/login"}, - Request: &apimodel.LoginRequest{}, - Response: &apimodel.LoginResponse{}, -}, { - Name: "MeasurementMeta", - Method: "GET", - URLPath: URLPath{Value: "/api/v1/measurement_meta"}, - Request: &apimodel.MeasurementMetaRequest{}, - Response: &apimodel.MeasurementMetaResponse{}, - CachePolicy: CacheAlways, -}, { - Name: "Register", - Method: "POST", - URLPath: URLPath{Value: "/api/v1/register"}, - Request: &apimodel.RegisterRequest{}, - Response: &apimodel.RegisterResponse{}, -}, { - Name: "TestHelpers", - Method: "GET", - URLPath: URLPath{Value: "/api/v1/test-helpers"}, - Request: &apimodel.TestHelpersRequest{}, - Response: apimodel.TestHelpersResponse{}, -}, { - Name: "PsiphonConfig", - RequiresLogin: true, - Method: "GET", - URLPath: URLPath{Value: "/api/v1/test-list/psiphon-config"}, - Request: &apimodel.PsiphonConfigRequest{}, - Response: apimodel.PsiphonConfigResponse{}, -}, { - Name: "TorTargets", - RequiresLogin: true, - Method: "GET", - URLPath: URLPath{Value: "/api/v1/test-list/tor-targets"}, - Request: &apimodel.TorTargetsRequest{}, - Response: apimodel.TorTargetsResponse{}, -}, { - Name: "URLs", - Method: "GET", - URLPath: URLPath{Value: "/api/v1/test-list/urls"}, - Request: &apimodel.URLsRequest{}, - Response: &apimodel.URLsResponse{}, -}, { - Name: "OpenReport", - Method: "POST", - URLPath: URLPath{Value: "/report"}, - Request: &apimodel.OpenReportRequest{}, - Response: &apimodel.OpenReportResponse{}, -}, { - Name: "SubmitMeasurement", - Method: "POST", - URLPath: URLPath{ - InSwagger: "/report/{report_id}", - IsTemplate: true, - Value: "/report/{{ .ReportID }}", - }, - Request: &apimodel.SubmitMeasurementRequest{}, - Response: &apimodel.SubmitMeasurementResponse{}, -}} diff --git a/internal/ooapi/internal/generator/swaggertest.go b/internal/ooapi/internal/generator/swaggertest.go deleted file mode 100644 index 96ed826..0000000 --- a/internal/ooapi/internal/generator/swaggertest.go +++ /dev/null @@ -1,194 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "log" - "reflect" - "strings" - "sync" - "time" - - "github.com/ooni/probe-cli/v3/internal/ooapi/internal/openapi" -) - -const ( - tagForJSON = "json" - tagForPath = "path" -) - -func (d *Descriptor) genSwaggerURLPath() string { - up := d.URLPath - if up.InSwagger != "" { - return up.InSwagger - } - if up.IsTemplate { - panic("we should always use InSwapper and IsTemplate together") - } - return up.Value -} - -func (d *Descriptor) genSwaggerSchema(cur reflect.Type) *openapi.Schema { - switch cur.Kind() { - case reflect.String: - return &openapi.Schema{Type: "string"} - case reflect.Bool: - return &openapi.Schema{Type: "boolean"} - case reflect.Int64: - return &openapi.Schema{Type: "integer"} - case reflect.Slice: - return &openapi.Schema{Type: "array", Items: d.genSwaggerSchema(cur.Elem())} - case reflect.Map: - return &openapi.Schema{Type: "object"} - case reflect.Ptr: - return d.genSwaggerSchema(cur.Elem()) - case reflect.Struct: - if cur.String() == "time.Time" { - // Implementation note: we don't want to dive into time.Time but - // rather we want to pretend it's a string. The JSON parser for - // time.Time can indeed reconstruct a time.Time from a string, and - // it's much easier for us to let it do the parsing. - return &openapi.Schema{Type: "string"} - } - sinfo := &openapi.Schema{Type: "object"} - var once sync.Once - initmap := func() { - sinfo.Properties = make(map[string]*openapi.Schema) - } - for idx := 0; idx < cur.NumField(); idx++ { - field := cur.Field(idx) - if field.Tag.Get(tagForPath) != "" { - continue // skipping because this is a path param - } - if field.Tag.Get(tagForQuery) != "" { - continue // skipping because this is a query param - } - v := field.Name - if j := field.Tag.Get(tagForJSON); j != "" { - j = strings.Replace(j, ",omitempty", "", 1) // remove options - if j == "-" { - continue // not exported via JSON - } - v = j - } - once.Do(initmap) - sinfo.Properties[v] = d.genSwaggerSchema(field.Type) - } - return sinfo - case reflect.Interface: - return &openapi.Schema{Type: "object"} - default: - panic("unsupported type") - } -} - -func (d *Descriptor) swaggerParamForType(t reflect.Type) string { - switch t.Kind() { - case reflect.String: - return "string" - case reflect.Bool: - return "boolean" - case reflect.Int64: - return "integer" - default: - panic("unsupported type") - } -} - -func (d *Descriptor) genSwaggerParams(cur reflect.Type) []*openapi.Parameter { - // when we have params the input must be a pointer to struct - if cur.Kind() != reflect.Ptr { - panic("not a pointer") - } - cur = cur.Elem() - if cur.Kind() != reflect.Struct { - panic("not a pointer to struct") - } - // now that we're sure of the type, inspect the fields - var out []*openapi.Parameter - for idx := 0; idx < cur.NumField(); idx++ { - f := cur.Field(idx) - if q := f.Tag.Get(tagForQuery); q != "" { - out = append( - out, &openapi.Parameter{ - Name: q, - In: "query", - Required: f.Tag.Get(tagForRequired) == "true", - Type: d.swaggerParamForType(f.Type), - }) - continue - } - if p := f.Tag.Get(tagForPath); p != "" { - out = append(out, &openapi.Parameter{ - Name: p, - In: "path", - Required: true, - Type: d.swaggerParamForType(f.Type), - }) - continue - } - } - return out -} - -func (d *Descriptor) genSwaggerPath() (string, *openapi.Path) { - pathStr, pathInfo := d.genSwaggerURLPath(), &openapi.Path{} - rtinfo := &openapi.RoundTrip{Produces: []string{"application/json"}} - switch d.Method { - case "GET": - pathInfo.Get = rtinfo - case "POST": - rtinfo.Consumes = append(rtinfo.Consumes, "application/json") - pathInfo.Post = rtinfo - default: - panic("unsupported method") - } - rtinfo.Parameters = d.genSwaggerParams(reflect.TypeOf(d.Request)) - if d.Method != "GET" { - rtinfo.Parameters = append(rtinfo.Parameters, &openapi.Parameter{ - Name: "body", - In: "body", - Required: true, - Schema: d.genSwaggerSchema(reflect.TypeOf(d.Request)), - }) - } - rtinfo.Responses = &openapi.Responses{Successful: openapi.Body{ - Description: "all good", - Schema: d.genSwaggerSchema(reflect.TypeOf(d.Response)), - }} - return pathStr, pathInfo -} - -func genSwaggerVersion() string { - return time.Now().UTC().Format("0.20060102.1150405") -} - -// GenSwaggerTestGo generates swagger_test.go -func GenSwaggerTestGo(file string) { - swagger := openapi.Swagger{ - Swagger: "2.0", - Info: openapi.API{ - Title: "OONI API specification", - Version: genSwaggerVersion(), - }, - Host: "api.ooni.io", - BasePath: "/", - Schemes: []string{"https"}, - Paths: make(map[string]*openapi.Path), - } - for _, desc := range Descriptors { - pathStr, pathInfo := desc.genSwaggerPath() - swagger.Paths[pathStr] = pathInfo - } - data, err := json.MarshalIndent(swagger, "", " ") - if err != nil { - log.Fatal(err) - } - var sb strings.Builder - fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n") - fmt.Fprintf(&sb, "// %s\n\n", time.Now()) - fmt.Fprint(&sb, "package ooapi\n\n") - fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file) - fmt.Fprintf(&sb, "const swagger = `%s`\n", string(data)) - writefile(file, &sb) -} diff --git a/internal/ooapi/internal/generator/writefile.go b/internal/ooapi/internal/generator/writefile.go deleted file mode 100644 index 48bbf2a..0000000 --- a/internal/ooapi/internal/generator/writefile.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "log" - "os" - "strings" - - "golang.org/x/sys/execabs" -) - -func writefile(name string, sb *strings.Builder) { - filep, err := os.Create(name) - if err != nil { - log.Fatal(err) - } - if _, err := fmt.Fprint(filep, sb.String()); err != nil { - log.Fatal(err) - } - if err := filep.Close(); err != nil { - log.Fatal(err) - } - cmd := execabs.Command("go", "fmt", name) - if err := cmd.Run(); err != nil { - log.Fatal(err) - } -} diff --git a/internal/ooapi/internal/openapi/openapi.go b/internal/ooapi/internal/openapi/openapi.go deleted file mode 100644 index d20d4bb..0000000 --- a/internal/ooapi/internal/openapi/openapi.go +++ /dev/null @@ -1,64 +0,0 @@ -// Package openapi contains data structures for Swagger v2.0. -// -// We use these data structures to compare the API specification we -// have here with the one of the server. -package openapi - -// Schema is the schema of a specific parameter or -// or the schema used by the response body -type Schema struct { - Properties map[string]*Schema `json:"properties,omitempty"` - Items *Schema `json:"items,omitempty"` - Type string `json:"type"` -} - -// Parameter describes an input parameter, which could be in the -// URL path, in the query string, or in the request body -type Parameter struct { - In string `json:"in"` - Name string `json:"name"` - Required bool `json:"required,omitempty"` - Schema *Schema `json:"schema,omitempty"` - Type string `json:"type,omitempty"` -} - -// Body describes a response body -type Body struct { - Description interface{} `json:"description,omitempty"` - Schema *Schema `json:"schema"` -} - -// Responses describes the possible responses -type Responses struct { - Successful Body `json:"200"` -} - -// RoundTrip describes an HTTP round trip with a given method and path -type RoundTrip struct { - Consumes []string `json:"consumes,omitempty"` - Produces []string `json:"produces,omitempty"` - Parameters []*Parameter `json:"parameters,omitempty"` - Responses *Responses `json:"responses,omitempty"` -} - -// Path describes a path served by the API -type Path struct { - Get *RoundTrip `json:"get,omitempty"` - Post *RoundTrip `json:"post,omitempty"` -} - -// API contains info about the API -type API struct { - Title string `json:"title"` - Version string `json:"version"` -} - -// Swagger is the toplevel structure -type Swagger struct { - Swagger string `json:"swagger"` - Info API `json:"info"` - Host string `json:"host"` - BasePath string `json:"basePath"` - Schemes []string `json:"schemes"` - Paths map[string]*Path `json:"paths"` -} diff --git a/internal/ooapi/login.go b/internal/ooapi/login.go deleted file mode 100644 index a3b5c8b..0000000 --- a/internal/ooapi/login.go +++ /dev/null @@ -1,295 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:49.900013 +0200 CEST m=+0.000719959 - -package ooapi - -//go:generate go run ./internal/generator -file login.go - -import ( - "context" - "errors" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -// withLoginPsiphonConfigAPI implements login for simplePsiphonConfigAPI. -type withLoginPsiphonConfigAPI struct { - API clonerForPsiphonConfigAPI // mandatory - JSONCodec JSONCodec // optional - KVStore KVStore // mandatory - RegisterAPI callerForRegisterAPI // mandatory - LoginAPI callerForLoginAPI // mandatory -} - -// Call logins, if needed, then calls the API. -func (api *withLoginPsiphonConfigAPI) Call(ctx context.Context, req *apimodel.PsiphonConfigRequest) (apimodel.PsiphonConfigResponse, error) { - token, err := api.maybeLogin(ctx) - if err != nil { - return nil, err - } - resp, err := api.API.WithToken(token).Call(ctx, req) - if errors.Is(err, ErrUnauthorized) { - // Maybe the clock is just off? Let's try to obtain - // a token again and see if this fixes it. - if token, err = api.forceLogin(ctx); err == nil { - switch resp, err = api.API.WithToken(token).Call(ctx, req); err { - case nil: - return resp, nil - case ErrUnauthorized: - // fallthrough - default: - return nil, err - } - } - // Okay, this seems a broader problem. How about we try - // and re-register ourselves again instead? - token, err = api.forceRegister(ctx) - if err != nil { - return nil, err - } - resp, err = api.API.WithToken(token).Call(ctx, req) - // fallthrough - } - if err != nil { - return nil, err - } - return resp, nil -} - -func (api *withLoginPsiphonConfigAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *withLoginPsiphonConfigAPI) readstate() (*loginState, error) { - data, err := api.KVStore.Get(loginKey) - if err != nil { - return nil, err - } - var ls loginState - if err := api.jsonCodec().Decode(data, &ls); err != nil { - return nil, err - } - return &ls, nil -} - -func (api *withLoginPsiphonConfigAPI) writestate(ls *loginState) error { - data, err := api.jsonCodec().Encode(*ls) - if err != nil { - return err - } - return api.KVStore.Set(loginKey, data) -} - -func (api *withLoginPsiphonConfigAPI) doRegister(ctx context.Context, password string) (string, error) { - req := newRegisterRequest(password) - ls := &loginState{} - resp, err := api.RegisterAPI.Call(ctx, req) - if err != nil { - return "", err - } - ls.ClientID = resp.ClientID - ls.Password = req.Password - return api.doLogin(ctx, ls) -} - -func (api *withLoginPsiphonConfigAPI) forceRegister(ctx context.Context) (string, error) { - var password string - // If we already have a previous password, let us keep - // using it. This will allow a new version of the API to - // be able to continue to identify this probe. (This - // assumes that we have a stateless API that generates - // the user ID as a signature of the password plus a - // timestamp and that the key to generate the signature - // is not lost. If all these conditions are met, we - // can then serve better test targets to more long running - // (and therefore trusted) probes.) - if ls, err := api.readstate(); err == nil { - password = ls.Password - } - if password == "" { - password = newRandomPassword() - } - return api.doRegister(ctx, password) -} - -func (api *withLoginPsiphonConfigAPI) forceLogin(ctx context.Context) (string, error) { - ls, err := api.readstate() - if err != nil { - return "", err - } - return api.doLogin(ctx, ls) -} - -func (api *withLoginPsiphonConfigAPI) maybeLogin(ctx context.Context) (string, error) { - ls, _ := api.readstate() - if ls == nil || !ls.credentialsValid() { - return api.forceRegister(ctx) - } - if !ls.tokenValid() { - return api.doLogin(ctx, ls) - } - return ls.Token, nil -} - -func (api *withLoginPsiphonConfigAPI) doLogin(ctx context.Context, ls *loginState) (string, error) { - req := &apimodel.LoginRequest{ - ClientID: ls.ClientID, - Password: ls.Password, - } - resp, err := api.LoginAPI.Call(ctx, req) - if err != nil { - return "", err - } - ls.Token = resp.Token - ls.Expire = resp.Expire - if err := api.writestate(ls); err != nil { - return "", err - } - return ls.Token, nil -} - -var _ callerForPsiphonConfigAPI = &withLoginPsiphonConfigAPI{} - -// withLoginTorTargetsAPI implements login for simpleTorTargetsAPI. -type withLoginTorTargetsAPI struct { - API clonerForTorTargetsAPI // mandatory - JSONCodec JSONCodec // optional - KVStore KVStore // mandatory - RegisterAPI callerForRegisterAPI // mandatory - LoginAPI callerForLoginAPI // mandatory -} - -// Call logins, if needed, then calls the API. -func (api *withLoginTorTargetsAPI) Call(ctx context.Context, req *apimodel.TorTargetsRequest) (apimodel.TorTargetsResponse, error) { - token, err := api.maybeLogin(ctx) - if err != nil { - return nil, err - } - resp, err := api.API.WithToken(token).Call(ctx, req) - if errors.Is(err, ErrUnauthorized) { - // Maybe the clock is just off? Let's try to obtain - // a token again and see if this fixes it. - if token, err = api.forceLogin(ctx); err == nil { - switch resp, err = api.API.WithToken(token).Call(ctx, req); err { - case nil: - return resp, nil - case ErrUnauthorized: - // fallthrough - default: - return nil, err - } - } - // Okay, this seems a broader problem. How about we try - // and re-register ourselves again instead? - token, err = api.forceRegister(ctx) - if err != nil { - return nil, err - } - resp, err = api.API.WithToken(token).Call(ctx, req) - // fallthrough - } - if err != nil { - return nil, err - } - return resp, nil -} - -func (api *withLoginTorTargetsAPI) jsonCodec() JSONCodec { - if api.JSONCodec != nil { - return api.JSONCodec - } - return &defaultJSONCodec{} -} - -func (api *withLoginTorTargetsAPI) readstate() (*loginState, error) { - data, err := api.KVStore.Get(loginKey) - if err != nil { - return nil, err - } - var ls loginState - if err := api.jsonCodec().Decode(data, &ls); err != nil { - return nil, err - } - return &ls, nil -} - -func (api *withLoginTorTargetsAPI) writestate(ls *loginState) error { - data, err := api.jsonCodec().Encode(*ls) - if err != nil { - return err - } - return api.KVStore.Set(loginKey, data) -} - -func (api *withLoginTorTargetsAPI) doRegister(ctx context.Context, password string) (string, error) { - req := newRegisterRequest(password) - ls := &loginState{} - resp, err := api.RegisterAPI.Call(ctx, req) - if err != nil { - return "", err - } - ls.ClientID = resp.ClientID - ls.Password = req.Password - return api.doLogin(ctx, ls) -} - -func (api *withLoginTorTargetsAPI) forceRegister(ctx context.Context) (string, error) { - var password string - // If we already have a previous password, let us keep - // using it. This will allow a new version of the API to - // be able to continue to identify this probe. (This - // assumes that we have a stateless API that generates - // the user ID as a signature of the password plus a - // timestamp and that the key to generate the signature - // is not lost. If all these conditions are met, we - // can then serve better test targets to more long running - // (and therefore trusted) probes.) - if ls, err := api.readstate(); err == nil { - password = ls.Password - } - if password == "" { - password = newRandomPassword() - } - return api.doRegister(ctx, password) -} - -func (api *withLoginTorTargetsAPI) forceLogin(ctx context.Context) (string, error) { - ls, err := api.readstate() - if err != nil { - return "", err - } - return api.doLogin(ctx, ls) -} - -func (api *withLoginTorTargetsAPI) maybeLogin(ctx context.Context) (string, error) { - ls, _ := api.readstate() - if ls == nil || !ls.credentialsValid() { - return api.forceRegister(ctx) - } - if !ls.tokenValid() { - return api.doLogin(ctx, ls) - } - return ls.Token, nil -} - -func (api *withLoginTorTargetsAPI) doLogin(ctx context.Context, ls *loginState) (string, error) { - req := &apimodel.LoginRequest{ - ClientID: ls.ClientID, - Password: ls.Password, - } - resp, err := api.LoginAPI.Call(ctx, req) - if err != nil { - return "", err - } - ls.Token = resp.Token - ls.Expire = resp.Expire - if err := api.writestate(ls); err != nil { - return "", err - } - return ls.Token, nil -} - -var _ callerForTorTargetsAPI = &withLoginTorTargetsAPI{} diff --git a/internal/ooapi/login_test.go b/internal/ooapi/login_test.go deleted file mode 100644 index c71c19c..0000000 --- a/internal/ooapi/login_test.go +++ /dev/null @@ -1,1433 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:50.443807 +0200 CEST m=+0.000781168 - -package ooapi - -//go:generate go run ./internal/generator -file login_test.go - -import ( - "context" - "errors" - "net/http/httptest" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/ooni/probe-cli/v3/internal/atomicx" - "github.com/ooni/probe-cli/v3/internal/kvstore" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func TestRegisterAndLoginPsiphonConfigSuccess(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigContinueUsingToken(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } - } - // step 2: we disable register and login but we - // should be okay because of the token - errMocked := errors.New("mocked error") - registerAPI.Err = errMocked - registerAPI.Response = nil - loginAPI.Err = errMocked - loginAPI.Response = nil - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigWithValidButExpiredToken(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - registerAPI := &FakeRegisterAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - ls := &loginState{ - ClientID: "antani-antani", - Expire: time.Now().Add(-5 * time.Second), - Token: "antani-antani-token", - Password: "antani-antani-password", - } - if err := login.writestate(ls); err != nil { - t.Fatal(err) - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 0 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigWithRegisterAPIError(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - registerAPI := &FakeRegisterAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigWithLoginFailure(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - loginAPI := &FakeLoginAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestRegisterAndLoginPsiphonConfigThenFail(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Err: errMocked, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigTheDatabaseIsReplaced(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simplePsiphonConfigAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginPsiphonConfigAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget accounts and try again. - handler.forgetLogins() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 3 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestRegisterAndLoginPsiphonConfigCannotWriteState(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - login := &withLoginPsiphonConfigAPI{ - API: &FakePsiphonConfigAPI{ - WithResult: &FakePsiphonConfigAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - JSONCodec: &FakeCodec{ - EncodeErr: errMocked, - }, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestPsiphonConfigReadStateDecodeFailure(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.PsiphonConfigResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - login := &withLoginPsiphonConfigAPI{ - KVStore: &kvstore.Memory{}, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ls := &loginState{ - ClientID: "antani-antani", - Expire: time.Now().Add(-5 * time.Second), - Token: "antani-antani-token", - Password: "antani-antani-password", - } - if err := login.writestate(ls); err != nil { - t.Fatal(err) - } - out, err := login.forceLogin(context.Background()) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if out != "" { - t.Fatal("expected empty string here") - } -} - -func TestPsiphonConfigTheDatabaseIsReplacedThenFailure(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simplePsiphonConfigAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginPsiphonConfigAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget accounts and try again. - // but registrations are also failing. - handler.forgetLogins() - handler.noRegister = true - resp, err := login.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestPsiphonConfigClockIsOffThenSuccess(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simplePsiphonConfigAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginPsiphonConfigAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } -} - -func TestPsiphonConfigClockIsOffThen401(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simplePsiphonConfigAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginPsiphonConfigAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - handler.failCallWith = []int{401, 401} - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal("not the error we expected", err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 3 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestPsiphonConfigClockIsOffThen500(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simplePsiphonConfigAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginPsiphonConfigAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.PsiphonConfigRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - handler.failCallWith = []int{401, 500} - resp, err := login.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } -} - -func TestRegisterAndLoginTorTargetsSuccess(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsContinueUsingToken(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } - } - // step 2: we disable register and login but we - // should be okay because of the token - errMocked := errors.New("mocked error") - registerAPI.Err = errMocked - registerAPI.Response = nil - loginAPI.Err = errMocked - loginAPI.Response = nil - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsWithValidButExpiredToken(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - registerAPI := &FakeRegisterAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - ls := &loginState{ - ClientID: "antani-antani", - Expire: time.Now().Add(-5 * time.Second), - Token: "antani-antani-token", - Password: "antani-antani-password", - } - if err := login.writestate(ls); err != nil { - t.Fatal(err) - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if diff := cmp.Diff(expect, resp); diff != "" { - t.Fatal(diff) - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 0 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsWithRegisterAPIError(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - registerAPI := &FakeRegisterAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsWithLoginFailure(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - loginAPI := &FakeLoginAPI{ - Err: errMocked, - CountCall: &atomicx.Int64{}, - } - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestRegisterAndLoginTorTargetsThenFail(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Err: errMocked, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsTheDatabaseIsReplaced(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simpleTorTargetsAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginTorTargetsAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget accounts and try again. - handler.forgetLogins() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 3 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestRegisterAndLoginTorTargetsCannotWriteState(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - registerAPI := &FakeRegisterAPI{ - Response: &apimodel.RegisterResponse{ - ClientID: "antani-antani", - }, - CountCall: &atomicx.Int64{}, - } - loginAPI := &FakeLoginAPI{ - Response: &apimodel.LoginResponse{ - Expire: time.Now().Add(3600 * time.Second), - Token: "antani-antani-token", - }, - CountCall: &atomicx.Int64{}, - } - errMocked := errors.New("mocked error") - login := &withLoginTorTargetsAPI{ - API: &FakeTorTargetsAPI{ - WithResult: &FakeTorTargetsAPI{ - Response: expect, - }, - }, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - JSONCodec: &FakeCodec{ - EncodeErr: errMocked, - }, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - resp, err := login.Call(ctx, req) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if loginAPI.CountCall.Load() != 1 { - t.Fatal("invalid loginAPI.CountCall") - } - if registerAPI.CountCall.Load() != 1 { - t.Fatal("invalid registerAPI.CountCall") - } -} - -func TestTorTargetsReadStateDecodeFailure(t *testing.T) { - ff := &fakeFill{} - var expect apimodel.TorTargetsResponse - ff.Fill(&expect) - errMocked := errors.New("mocked error") - login := &withLoginTorTargetsAPI{ - KVStore: &kvstore.Memory{}, - JSONCodec: &FakeCodec{DecodeErr: errMocked}, - } - ls := &loginState{ - ClientID: "antani-antani", - Expire: time.Now().Add(-5 * time.Second), - Token: "antani-antani-token", - Password: "antani-antani-password", - } - if err := login.writestate(ls); err != nil { - t.Fatal(err) - } - out, err := login.forceLogin(context.Background()) - if !errors.Is(err, errMocked) { - t.Fatal("not the error we expected", err) - } - if out != "" { - t.Fatal("expected empty string here") - } -} - -func TestTorTargetsTheDatabaseIsReplacedThenFailure(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simpleTorTargetsAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginTorTargetsAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget accounts and try again. - // but registrations are also failing. - handler.forgetLogins() - handler.noRegister = true - resp, err := login.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestTorTargetsClockIsOffThenSuccess(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simpleTorTargetsAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginTorTargetsAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } -} - -func TestTorTargetsClockIsOffThen401(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simpleTorTargetsAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginTorTargetsAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - handler.failCallWith = []int{401, 401} - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal("not the error we expected", err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 3 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 2 { - t.Fatal("invalid handler.registers") - } -} - -func TestTorTargetsClockIsOffThen500(t *testing.T) { - ff := &fakeFill{} - handler := &LoginHandler{ - logins: &atomicx.Int64{}, - registers: &atomicx.Int64{}, - t: t, - } - srvr := httptest.NewServer(handler) - defer srvr.Close() - registerAPI := &simpleRegisterAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - loginAPI := &simpleLoginAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - baseAPI := &simpleTorTargetsAPI{ - HTTPClient: &VerboseHTTPClient{T: t}, - BaseURL: srvr.URL, - } - login := &withLoginTorTargetsAPI{ - API: baseAPI, - RegisterAPI: registerAPI, - LoginAPI: loginAPI, - KVStore: &kvstore.Memory{}, - } - var req *apimodel.TorTargetsRequest - ff.Fill(&req) - ctx := context.Background() - // step 1: we register and login and use the token - // inside a scope just to avoid mistakes - { - resp, err := login.Call(ctx, req) - if err != nil { - t.Fatal(err) - } - if resp == nil { - t.Fatal("expected non-nil response") - } - if handler.logins.Load() != 1 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } - } - // step 2: we forget tokens and try again. - // this should simulate the client clock - // being off and considering a token still valid - handler.forgetTokens() - handler.failCallWith = []int{401, 500} - resp, err := login.Call(ctx, req) - if !errors.Is(err, ErrHTTPFailure) { - t.Fatal("not the error we expected", err) - } - if resp != nil { - t.Fatal("expected nil response") - } - if handler.logins.Load() != 2 { - t.Fatal("invalid handler.logins") - } - if handler.registers.Load() != 1 { - t.Fatal("invalid handler.registers") - } -} diff --git a/internal/ooapi/loginhandler_test.go b/internal/ooapi/loginhandler_test.go deleted file mode 100644 index b3d89d7..0000000 --- a/internal/ooapi/loginhandler_test.go +++ /dev/null @@ -1,208 +0,0 @@ -package ooapi - -import ( - "encoding/json" - "net/http" - "strings" - "sync" - "testing" - "time" - - "github.com/ooni/probe-cli/v3/internal/atomicx" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -// LoginHandler is an http.Handler to test login -type LoginHandler struct { - failCallWith []int // ignored by login and register - mu sync.Mutex - noRegister bool - state []*loginState - t *testing.T - logins *atomicx.Int64 - registers *atomicx.Int64 -} - -func (lh *LoginHandler) forgetLogins() { - defer lh.mu.Unlock() - lh.mu.Lock() - lh.state = nil -} - -func (lh *LoginHandler) forgetTokens() { - defer lh.mu.Unlock() - lh.mu.Lock() - for _, entry := range lh.state { - // This should be enough to cause all tokens to - // be expired and force clients to relogin. - // - // (It does not matter much whether the client - // clock is off, or the server clock is off, - // thanks Galileo for explaining this to us <3.) - entry.Expire = time.Now().Add(-3600 * time.Second) - } -} - -func (lh *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // Implementation note: we don't check for the method - // for simplicity since it's already tested. - switch r.URL.Path { - case "/api/v1/register": - if lh.registers != nil { - lh.registers.Add(1) - } - lh.register(w, r) - case "/api/v1/login": - if lh.logins != nil { - lh.logins.Add(1) - } - lh.login(w, r) - case "/api/v1/test-list/psiphon-config": - lh.psiphon(w, r) - case "/api/v1/test-list/tor-targets": - lh.tor(w, r) - default: - w.WriteHeader(500) - } -} - -func (lh *LoginHandler) register(w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - w.WriteHeader(400) - return - } - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - var req apimodel.RegisterRequest - if err := json.Unmarshal(data, &req); err != nil { - w.WriteHeader(400) - return - } - if req.Password == "" { - w.WriteHeader(400) - return - } - defer lh.mu.Unlock() - lh.mu.Lock() - if lh.noRegister { - // We have been asked to stop registering clients so - // we're going to make a boo boo. - w.WriteHeader(500) - return - } - var resp apimodel.RegisterResponse - ff := &fakeFill{} - ff.Fill(&resp) - lh.state = append(lh.state, &loginState{ - ClientID: resp.ClientID, Password: req.Password}) - data, err = json.Marshal(&resp) - if err != nil { - w.WriteHeader(500) - return - } - lh.t.Logf("register: %+v", string(data)) - w.Write(data) -} - -func (lh *LoginHandler) login(w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - w.WriteHeader(400) - return - } - data, err := netxlite.ReadAllContext(r.Context(), r.Body) - if err != nil { - w.WriteHeader(400) - return - } - var req apimodel.LoginRequest - if err := json.Unmarshal(data, &req); err != nil { - w.WriteHeader(400) - return - } - defer lh.mu.Unlock() - lh.mu.Lock() - for _, s := range lh.state { - if req.ClientID == s.ClientID && req.Password == s.Password { - var resp apimodel.LoginResponse - ff := &fakeFill{} - ff.Fill(&resp) - // We want the token to be many seconds in the future while - // ff.fill only sets the tokent to now plus a small delta. - resp.Expire = time.Now().Add(3600 * time.Second) - s.Expire = resp.Expire - s.Token = resp.Token - data, err = json.Marshal(&resp) - if err != nil { - w.WriteHeader(500) - return - } - lh.t.Logf("login: %+v", string(data)) - w.Write(data) - return - } - } - lh.t.Log("login: 401") - w.WriteHeader(401) -} - -func (lh *LoginHandler) psiphon(w http.ResponseWriter, r *http.Request) { - defer lh.mu.Unlock() - lh.mu.Lock() - if len(lh.failCallWith) > 0 { - code := lh.failCallWith[0] - lh.failCallWith = lh.failCallWith[1:] - w.WriteHeader(code) - return - } - token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1) - for _, s := range lh.state { - if token == s.Token && time.Now().Before(s.Expire) { - var resp apimodel.PsiphonConfigResponse - ff := &fakeFill{} - ff.Fill(&resp) - data, err := json.Marshal(&resp) - if err != nil { - w.WriteHeader(500) - return - } - lh.t.Logf("psiphon: %+v", string(data)) - w.Write(data) - return - } - } - lh.t.Log("psiphon: 401") - w.WriteHeader(401) -} - -func (lh *LoginHandler) tor(w http.ResponseWriter, r *http.Request) { - defer lh.mu.Unlock() - lh.mu.Lock() - if len(lh.failCallWith) > 0 { - code := lh.failCallWith[0] - lh.failCallWith = lh.failCallWith[1:] - w.WriteHeader(code) - return - } - token := strings.Replace(r.Header.Get("Authorization"), "Bearer ", "", 1) - for _, s := range lh.state { - if token == s.Token && time.Now().Before(s.Expire) { - var resp apimodel.TorTargetsResponse - ff := &fakeFill{} - ff.Fill(&resp) - data, err := json.Marshal(&resp) - if err != nil { - w.WriteHeader(500) - return - } - lh.t.Logf("tor: %+v", string(data)) - w.Write(data) - return - } - } - lh.t.Log("tor: 401") - w.WriteHeader(401) -} diff --git a/internal/ooapi/loginmodel.go b/internal/ooapi/loginmodel.go deleted file mode 100644 index 5d86a5c..0000000 --- a/internal/ooapi/loginmodel.go +++ /dev/null @@ -1,59 +0,0 @@ -package ooapi - -import ( - "crypto/rand" - "encoding/base64" - "time" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" - "github.com/ooni/probe-cli/v3/internal/runtimex" -) - -// loginState is the struct saved in the kvstore -// to keep track of the login state. -type loginState struct { - ClientID string - Expire time.Time - Password string - Token string -} - -func (ls *loginState) credentialsValid() bool { - return ls.ClientID != "" && ls.Password != "" -} - -func (ls *loginState) tokenValid() bool { - return ls.Token != "" && time.Now().Add(60*time.Second).Before(ls.Expire) -} - -// loginKey is the key with which loginState is saved -// into the key-value store used by Client. -const loginKey = "orchestra.state" - -// newRandomPassword generates a new random password. -func newRandomPassword() string { - b := make([]byte, 48) - _, err := rand.Read(b) - runtimex.PanicOnError(err, "rand.Read failed") - return base64.StdEncoding.EncodeToString(b) -} - -// newRegisterRequest creates a new RegisterRequest. -func newRegisterRequest(password string) *apimodel.RegisterRequest { - return &apimodel.RegisterRequest{ - // The original implementation has as its only use case that we - // were registering and logging in for sending an update regarding - // the probe whereabouts. Yet here in probe-engine, the orchestra - // is currently only used to fetch inputs. For this purpose, we don't - // need to communicate any specific information. The code that will - // perform an update used to be responsible of doing that. Now, we - // are not using orchestra for this purpose anymore. - Platform: "miniooni", - ProbeASN: "AS0", - ProbeCC: "ZZ", - SoftwareName: "miniooni", - SoftwareVersion: "0.1.0-dev", - SupportedTests: []string{"web_connectivity"}, - Password: password, - } -} diff --git a/internal/ooapi/requests.go b/internal/ooapi/requests.go deleted file mode 100644 index 1439cda..0000000 --- a/internal/ooapi/requests.go +++ /dev/null @@ -1,192 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:50.891059 +0200 CEST m=+0.000799292 - -package ooapi - -//go:generate go run ./internal/generator -file requests.go - -import ( - "bytes" - "context" - "net/http" - "net/url" - - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func (api *simpleCheckReportIDAPI) newRequest(ctx context.Context, req *apimodel.CheckReportIDRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/_/check_report_id" - q := url.Values{} - if req.ReportID == "" { - return nil, newErrEmptyField("ReportID") - } - q.Add("report_id", req.ReportID) - URL.RawQuery = q.Encode() - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simpleCheckInAPI) newRequest(ctx context.Context, req *apimodel.CheckInRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/check-in" - body, err := api.jsonCodec().Encode(req) - if err != nil { - return nil, err - } - out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - out.Header.Set("Content-Type", "application/json") - return out, nil -} - -func (api *simpleLoginAPI) newRequest(ctx context.Context, req *apimodel.LoginRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/login" - body, err := api.jsonCodec().Encode(req) - if err != nil { - return nil, err - } - out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - out.Header.Set("Content-Type", "application/json") - return out, nil -} - -func (api *simpleMeasurementMetaAPI) newRequest(ctx context.Context, req *apimodel.MeasurementMetaRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/measurement_meta" - q := url.Values{} - if req.ReportID == "" { - return nil, newErrEmptyField("ReportID") - } - q.Add("report_id", req.ReportID) - if req.Full { - q.Add("full", "true") - } - if req.Input != "" { - q.Add("input", req.Input) - } - URL.RawQuery = q.Encode() - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simpleRegisterAPI) newRequest(ctx context.Context, req *apimodel.RegisterRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/register" - body, err := api.jsonCodec().Encode(req) - if err != nil { - return nil, err - } - out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - out.Header.Set("Content-Type", "application/json") - return out, nil -} - -func (api *simpleTestHelpersAPI) newRequest(ctx context.Context, req *apimodel.TestHelpersRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/test-helpers" - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simplePsiphonConfigAPI) newRequest(ctx context.Context, req *apimodel.PsiphonConfigRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/test-list/psiphon-config" - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simpleTorTargetsAPI) newRequest(ctx context.Context, req *apimodel.TorTargetsRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/test-list/tor-targets" - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simpleURLsAPI) newRequest(ctx context.Context, req *apimodel.URLsRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/api/v1/test-list/urls" - q := url.Values{} - if req.CategoryCodes != "" { - q.Add("category_codes", req.CategoryCodes) - } - if req.CountryCode != "" { - q.Add("country_code", req.CountryCode) - } - if req.Limit != 0 { - q.Add("limit", newQueryFieldInt64(req.Limit)) - } - URL.RawQuery = q.Encode() - return api.requestMaker().NewRequest(ctx, "GET", URL.String(), nil) -} - -func (api *simpleOpenReportAPI) newRequest(ctx context.Context, req *apimodel.OpenReportRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - URL.Path = "/report" - body, err := api.jsonCodec().Encode(req) - if err != nil { - return nil, err - } - out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - out.Header.Set("Content-Type", "application/json") - return out, nil -} - -func (api *simpleSubmitMeasurementAPI) newRequest(ctx context.Context, req *apimodel.SubmitMeasurementRequest) (*http.Request, error) { - URL, err := url.Parse(api.baseURL()) - if err != nil { - return nil, err - } - up, err := api.templateExecutor().Execute("/report/{{ .ReportID }}", req) - if err != nil { - return nil, err - } - URL.Path = up - body, err := api.jsonCodec().Encode(req) - if err != nil { - return nil, err - } - out, err := api.requestMaker().NewRequest(ctx, "POST", URL.String(), bytes.NewReader(body)) - if err != nil { - return nil, err - } - out.Header.Set("Content-Type", "application/json") - return out, nil -} diff --git a/internal/ooapi/responses.go b/internal/ooapi/responses.go deleted file mode 100644 index 24b0ba2..0000000 --- a/internal/ooapi/responses.go +++ /dev/null @@ -1,277 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:51.316851 +0200 CEST m=+0.000816793 - -package ooapi - -//go:generate go run ./internal/generator -file responses.go - -import ( - "context" - "io" - "net/http" - - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel" -) - -func (api *simpleCheckReportIDAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.CheckReportIDResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.CheckReportIDResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleCheckInAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.CheckInResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.CheckInResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleLoginAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.LoginResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.LoginResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleMeasurementMetaAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.MeasurementMetaResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.MeasurementMetaResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleRegisterAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.RegisterResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.RegisterResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleTestHelpersAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.TestHelpersResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := apimodel.TestHelpersResponse{} - if err := api.jsonCodec().Decode(data, &out); err != nil { - return nil, err - } - if out == nil { - return nil, ErrJSONLiteralNull - } - return out, nil -} - -func (api *simplePsiphonConfigAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.PsiphonConfigResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := apimodel.PsiphonConfigResponse{} - if err := api.jsonCodec().Decode(data, &out); err != nil { - return nil, err - } - if out == nil { - return nil, ErrJSONLiteralNull - } - return out, nil -} - -func (api *simpleTorTargetsAPI) newResponse(ctx context.Context, resp *http.Response, err error) (apimodel.TorTargetsResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := apimodel.TorTargetsResponse{} - if err := api.jsonCodec().Decode(data, &out); err != nil { - return nil, err - } - if out == nil { - return nil, ErrJSONLiteralNull - } - return out, nil -} - -func (api *simpleURLsAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.URLsResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.URLsResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleOpenReportAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.OpenReportResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.OpenReportResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} - -func (api *simpleSubmitMeasurementAPI) newResponse(ctx context.Context, resp *http.Response, err error) (*apimodel.SubmitMeasurementResponse, error) { - if err != nil { - return nil, err - } - if resp.StatusCode == 401 { - return nil, ErrUnauthorized - } - if resp.StatusCode != 200 { - return nil, newHTTPFailure(resp.StatusCode) - } - defer resp.Body.Close() - reader := io.LimitReader(resp.Body, 4<<20) - data, err := netxlite.ReadAllContext(ctx, reader) - if err != nil { - return nil, err - } - out := &apimodel.SubmitMeasurementResponse{} - if err := api.jsonCodec().Decode(data, out); err != nil { - return nil, err - } - return out, nil -} diff --git a/internal/ooapi/swagger_test.go b/internal/ooapi/swagger_test.go deleted file mode 100644 index cb96f86..0000000 --- a/internal/ooapi/swagger_test.go +++ /dev/null @@ -1,578 +0,0 @@ -// Code generated by go generate; DO NOT EDIT. -// 2022-05-19 20:30:51.768205 +0200 CEST m=+0.001003793 - -package ooapi - -//go:generate go run ./internal/generator -file swagger_test.go - -const swagger = `{ - "swagger": "2.0", - "info": { - "title": "OONI API specification", - "version": "0.20220519.5183051" - }, - "host": "api.ooni.io", - "basePath": "/", - "schemes": [ - "https" - ], - "paths": { - "/api/_/check_report_id": { - "get": { - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "query", - "name": "report_id", - "required": true, - "type": "string" - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "error": { - "type": "string" - }, - "found": { - "type": "boolean" - }, - "v": { - "type": "integer" - } - }, - "type": "object" - } - } - } - } - }, - "/api/v1/check-in": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "required": true, - "schema": { - "properties": { - "charging": { - "type": "boolean" - }, - "on_wifi": { - "type": "boolean" - }, - "platform": { - "type": "string" - }, - "probe_asn": { - "type": "string" - }, - "probe_cc": { - "type": "string" - }, - "run_type": { - "type": "string" - }, - "software_name": { - "type": "string" - }, - "software_version": { - "type": "string" - }, - "web_connectivity": { - "properties": { - "category_codes": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - }, - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "probe_asn": { - "type": "string" - }, - "probe_cc": { - "type": "string" - }, - "tests": { - "properties": { - "web_connectivity": { - "properties": { - "report_id": { - "type": "string" - }, - "urls": { - "items": { - "properties": { - "category_code": { - "type": "string" - }, - "country_code": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - } - }, - "type": "object" - }, - "v": { - "type": "integer" - } - }, - "type": "object" - } - } - } - } - }, - "/api/v1/login": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "required": true, - "schema": { - "properties": { - "password": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "expire": { - "type": "string" - }, - "token": { - "type": "string" - } - }, - "type": "object" - } - } - } - } - }, - "/api/v1/measurement_meta": { - "get": { - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "query", - "name": "report_id", - "required": true, - "type": "string" - }, - { - "in": "query", - "name": "full", - "type": "boolean" - }, - { - "in": "query", - "name": "input", - "type": "string" - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "anomaly": { - "type": "boolean" - }, - "category_code": { - "type": "string" - }, - "confirmed": { - "type": "boolean" - }, - "failure": { - "type": "boolean" - }, - "input": { - "type": "string" - }, - "measurement_start_time": { - "type": "string" - }, - "probe_asn": { - "type": "integer" - }, - "probe_cc": { - "type": "string" - }, - "raw_measurement": { - "type": "string" - }, - "report_id": { - "type": "string" - }, - "scores": { - "type": "string" - }, - "test_name": { - "type": "string" - }, - "test_start_time": { - "type": "string" - } - }, - "type": "object" - } - } - } - } - }, - "/api/v1/register": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "required": true, - "schema": { - "properties": { - "available_bandwidth": { - "type": "string" - }, - "device_token": { - "type": "string" - }, - "language": { - "type": "string" - }, - "network_type": { - "type": "string" - }, - "password": { - "type": "string" - }, - "platform": { - "type": "string" - }, - "probe_asn": { - "type": "string" - }, - "probe_cc": { - "type": "string" - }, - "probe_family": { - "type": "string" - }, - "probe_timezone": { - "type": "string" - }, - "software_name": { - "type": "string" - }, - "software_version": { - "type": "string" - }, - "supported_tests": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "client_id": { - "type": "string" - } - }, - "type": "object" - } - } - } - } - }, - "/api/v1/test-helpers": { - "get": { - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/test-list/psiphon-config": { - "get": { - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/test-list/tor-targets": { - "get": { - "produces": [ - "application/json" - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "type": "object" - } - } - } - } - }, - "/api/v1/test-list/urls": { - "get": { - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "query", - "name": "category_codes", - "type": "string" - }, - { - "in": "query", - "name": "country_code", - "type": "string" - }, - { - "in": "query", - "name": "limit", - "type": "integer" - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "metadata": { - "properties": { - "count": { - "type": "integer" - } - }, - "type": "object" - }, - "results": { - "items": { - "properties": { - "category_code": { - "type": "string" - }, - "country_code": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "type": "object" - }, - "type": "array" - } - }, - "type": "object" - } - } - } - } - }, - "/report": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "required": true, - "schema": { - "properties": { - "data_format_version": { - "type": "string" - }, - "format": { - "type": "string" - }, - "probe_asn": { - "type": "string" - }, - "probe_cc": { - "type": "string" - }, - "software_name": { - "type": "string" - }, - "software_version": { - "type": "string" - }, - "test_name": { - "type": "string" - }, - "test_start_time": { - "type": "string" - }, - "test_version": { - "type": "string" - } - }, - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "backend_version": { - "type": "string" - }, - "report_id": { - "type": "string" - }, - "supported_formats": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "type": "object" - } - } - } - } - }, - "/report/{report_id}": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "path", - "name": "report_id", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "required": true, - "schema": { - "properties": { - "content": { - "type": "object" - }, - "format": { - "type": "string" - } - }, - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "all good", - "schema": { - "properties": { - "measurement_uid": { - "type": "string" - } - }, - "type": "object" - } - } - } - } - } - } -}` diff --git a/internal/ooapi/swaggerdiff_test.go b/internal/ooapi/swaggerdiff_test.go deleted file mode 100644 index bac6c2a..0000000 --- a/internal/ooapi/swaggerdiff_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package ooapi - -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "sort" - "strings" - "testing" - - "github.com/hexops/gotextdiff" - "github.com/hexops/gotextdiff/myers" - "github.com/hexops/gotextdiff/span" - "github.com/ooni/probe-cli/v3/internal/netxlite" - "github.com/ooni/probe-cli/v3/internal/ooapi/internal/openapi" -) - -const ( - productionURL = "https://api.ooni.io/apispec_1.json" - testingURL = "https://ams-pg-test.ooni.org/apispec_1.json" -) - -func makeModel(data []byte) *openapi.Swagger { - var out openapi.Swagger - if err := json.Unmarshal(data, &out); err != nil { - log.Fatal(err) - } - // We reduce irrelevant differences by producing a common header - return &openapi.Swagger{Paths: out.Paths} -} - -func getServerModel(serverURL string) *openapi.Swagger { - resp, err := http.Get(serverURL) - if err != nil { - log.Fatal(err) - } - defer resp.Body.Close() - data, err := netxlite.ReadAllContext(context.Background(), resp.Body) - if err != nil { - log.Fatal(err) - } - return makeModel(data) -} - -func getClientModel() *openapi.Swagger { - return makeModel([]byte(swagger)) -} - -func simplifyRoundTrip(rt *openapi.RoundTrip) { - // Normalize the used name when a parameter is in body. This - // should only have a cosmetic impact on the spec. - for _, param := range rt.Parameters { - if param.In == "body" { - param.Name = "body" - } - } - - // Sort parameters so the comparison does not depend on order. - sort.SliceStable(rt.Parameters, func(i, j int) bool { - left, right := rt.Parameters[i].Name, rt.Parameters[j].Name - return strings.Compare(left, right) < 0 - }) - - // Normalize description of 200 response - rt.Responses.Successful.Description = "all good" -} - -func simplifyInPlace(path *openapi.Path) *openapi.Path { - if path.Get != nil && path.Post != nil { - log.Fatal("unsupported configuration") - } - if path.Get != nil { - simplifyRoundTrip(path.Get) - } - if path.Post != nil { - simplifyRoundTrip(path.Post) - } - return path -} - -func jsonify(model interface{}) string { - data, err := json.MarshalIndent(model, "", " ") - if err != nil { - log.Fatal(err) - } - return string(data) -} - -type diffable struct { - name string - value string -} - -func computediff(server, client *diffable) string { - d := gotextdiff.ToUnified(server.name, client.name, server.value, myers.ComputeEdits( - span.URIFromPath(server.name), server.value, client.value, - )) - return fmt.Sprint(d) -} - -// maybediff emits the diff between the server and the client and -// returns the length of the diff itself in bytes. -func maybediff(key string, server, client *openapi.Path) int { - diff := computediff(&diffable{ - name: fmt.Sprintf("server%s.json", key), - value: jsonify(simplifyInPlace(server)), - }, &diffable{ - name: fmt.Sprintf("client%s.json", key), - value: jsonify(simplifyInPlace(client)), - }) - if diff != "" { - fmt.Printf("%s", diff) - } - return len(diff) -} - -func compare(serverURL string) bool { - good := true - serverModel, clientModel := getServerModel(serverURL), getClientModel() - // Implementation note: the server model is richer than the client - // model, so we ignore everything not defined by the client. - var count int - for key := range serverModel.Paths { - if _, found := clientModel.Paths[key]; !found { - delete(serverModel.Paths, key) - continue - } - count++ - if maybediff(key, serverModel.Paths[key], clientModel.Paths[key]) > 0 { - good = false - } - } - if count <= 0 { - panic("no element found") - } - return good -} - -func TestWithProductionAPI(t *testing.T) { - t.Skip("skip until we use this part of the codebase") - if testing.Short() { - t.Skip("skip test in short mode") - } - t.Log("using ", productionURL) - if !compare(productionURL) { - t.Fatal("model mismatch (see above)") - } -} - -func TestWithTestingAPI(t *testing.T) { - t.Skip("skip until we use this part of the codebase") - if testing.Short() { - t.Skip("skip test in short mode") - } - t.Log("using ", testingURL) - if !compare(testingURL) { - t.Fatal("model mismatch (see above)") - } -} diff --git a/internal/ooapi/utils.go b/internal/ooapi/utils.go deleted file mode 100644 index 6d93a0b..0000000 --- a/internal/ooapi/utils.go +++ /dev/null @@ -1,23 +0,0 @@ -package ooapi - -import "fmt" - -func newErrEmptyField(field string) error { - return fmt.Errorf("%w: %s", ErrEmptyField, field) -} - -func newHTTPFailure(status int) error { - return fmt.Errorf("%w: %d", ErrHTTPFailure, status) -} - -func newQueryFieldInt64(v int64) string { - return fmt.Sprintf("%d", v) -} - -func newQueryFieldBool(v bool) string { - return fmt.Sprintf("%v", v) -} - -func newAuthorizationHeader(token string) string { - return fmt.Sprintf("Bearer %s", token) -} diff --git a/internal/ooapi/utils_test.go b/internal/ooapi/utils_test.go deleted file mode 100644 index 54b158b..0000000 --- a/internal/ooapi/utils_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package ooapi - -import "testing" - -func TestNewQueryFieldBoolWorks(t *testing.T) { - if s := newQueryFieldBool(true); s != "true" { - t.Fatal("invalid encoding of true") - } - if s := newQueryFieldBool(false); s != "false" { - t.Fatal("invalid encoding of false") - } -}