From e12c9c410f59c30d196bf513aa45555322846a88 Mon Sep 17 00:00:00 2001 From: Peter Eisenlohr Date: Mon, 9 Nov 2026 21:34:51 +0000 Subject: [PATCH 1/3] build: move the anyhttp pin to master bc985a0 Brings what anyhttp has on cancellation (pgit/anyhttp#41, #42, #44). Capy and Corosio stay where they are, at anyhttp's pins. Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8747b36..76f486c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,7 +48,7 @@ set(C_ARES_ASYNC_TLS ${TLS_LIBRARY}) include(FetchContent) FetchContent_Declare(anyhttp GIT_REPOSITORY https://github.com/pgit/anyhttp.git - GIT_TAG 296e8ee11f2bec37aa1bf5edf345017d888e381a + GIT_TAG bc985a0e040d8b2dd027552b964bde19b0fbf38a SYSTEM) FetchContent_Declare(c_ares_async GIT_REPOSITORY https://github.com/pgit/c-ares-async.git From b10879e4391948b0b7f9cf8d9f752b2ceaa9c851 Mon Sep 17 00:00:00 2001 From: Peter Eisenlohr Date: Mon, 9 Nov 2026 21:34:51 +0000 Subject: [PATCH 2/3] build: move the c-ares-async pin to master 2bbeb48, which is headers only c_ares_async::c_ares_async is no more: a program links the target of its runtime, here c_ares_async::corosio, and so does the fake DNS server the tests borrow. C_ARES_ASYNC_API only has c-ares-async look for Corosio now, rather than choose the runtime of its targets. Its Capy and Corosio pins are those of anyhttp again (pgit/c-ares-async#9). Co-Authored-By: Claude Opus 5.5 --- CMakeLists.txt | 10 ++++++---- test/CMakeLists.txt | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 76f486c..3c9f5e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,9 @@ endif() # # I/O runtime: Capy and Corosio, and only those. The libraries take either, which is their business: # this is a program, and it gets to decide. These shadow the cache entries of the libraries, which -# go on saying ASIO in CMakeCache.txt. +# go on saying ASIO in CMakeCache.txt. c-ares-async is headers, and its runtime is the target a +# program links, c_ares_async::corosio here; its option only has it look for Corosio rather than +# insist on Asio. # set(ANYHTTP_API COROSIO) set(C_ARES_ASYNC_API ${ANYHTTP_API}) @@ -52,12 +54,12 @@ FetchContent_Declare(anyhttp SYSTEM) FetchContent_Declare(c_ares_async GIT_REPOSITORY https://github.com/pgit/c-ares-async.git - GIT_TAG 6d46c6c8e65231f23a250bcec5dd00b3a1d9ba34 + GIT_TAG 2bbeb48fcd7ca539fc1c6f3fc580984cde7ada4b SYSTEM) # # Both libraries pin these as well. The first declaration wins, so these are the ones both of them -# get, and here they are SYSTEM, too. They follow anyhttp's pins, which c-ares-async's lag behind. +# get, and here they are SYSTEM, too. They are anyhttp's pins, and c-ares-async has the same. # FetchContent_Declare(capy GIT_REPOSITORY https://github.com/cppalliance/capy.git @@ -85,7 +87,7 @@ set(http_probe_warnings -Wall -Wextra -Werror -Wno-missing-designated-field-init add_library(http_probe_core STATIC src/output.cpp src/prober.cpp src/routes.cpp) target_include_directories(http_probe_core PUBLIC src) -target_link_libraries(http_probe_core PUBLIC anyhttp::anyhttp c_ares_async::c_ares_async) +target_link_libraries(http_probe_core PUBLIC anyhttp::anyhttp c_ares_async::corosio) target_compile_options(http_probe_core PRIVATE ${http_probe_warnings}) add_executable(http-probe src/main.cpp) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 745179e..093757b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,7 +21,7 @@ add_library( fake_dns STATIC ${c_ares_async_SOURCE_DIR}/test/fake_dns_server.cpp ${c_ares_async_SOURCE_DIR}/test/test_certificate.cpp) target_include_directories(fake_dns SYSTEM PUBLIC ${c_ares_async_SOURCE_DIR}/test) -target_link_libraries(fake_dns PUBLIC c_ares_async::c_ares_async anyhttp::anyhttp OpenSSL::SSL +target_link_libraries(fake_dns PUBLIC c_ares_async::corosio anyhttp::anyhttp OpenSSL::SSL OpenSSL::Crypto) add_library(test_site STATIC site.cpp) From 35827cf36ee5ef3a15a4e38a15fa4dc22f0aa924 Mon Sep 17 00:00:00 2001 From: Peter Eisenlohr Date: Mon, 9 Nov 2026 22:39:18 +0000 Subject: [PATCH 3/3] feat: print the header fields only with --all-headers Whether the header fields are printed was up to --each-address, which is about how many requests go out, not about how much of each response is shown. Now every response gets its status line by default, and --all-headers adds its header fields below it, for one request per host as for one per address. For that, the reply of each address keeps its header fields as well. format_result() takes a Style for the colour and the header fields, in place of a bool. Co-Authored-By: Claude Opus 5.5 --- README.md | 9 ++-- doc/README.md | 2 +- src/main.cpp | 12 +++-- src/output.cpp | 22 ++++++-- src/output.hpp | 12 ++++- src/prober.cpp | 12 ++++- src/prober.hpp | 6 ++- test/CMakeLists.txt | 7 +++ test/output_test.cpp | 123 +++++++++++++++++++++++++++++-------------- test/prober_test.cpp | 1 + 10 files changed, 150 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index bcd6c43..02f632a 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ http-probe --dot --dns-server one.one.one.one cloudflare.com It looks up the HTTPS records of each host in DNS ([RFC 9460](https://www.rfc-editor.org/rfc/rfc9460)), and requests `https:///` once, over TLS, in the way that the records say. It prints the status, -the route, the IP address it connected to, the time it took and the header fields of the response, -along with the host's records. A redirect is reported and not followed, and nothing else on the host -is requested. Its `Location` goes at the end of the status line, as in `301 … -> https://www.example.com/`, +the route, the IP address it connected to and the time it took, on one line, along with the host's +records. `--all-headers` adds the header fields of the response below that line. A redirect is +reported and not followed, and nothing else on the host is requested. Its `Location` goes at the end of the status line, as in `301 … -> https://www.example.com/`, as it came: not resolved against the URL, and not looked up. [Here is what that looks like](doc/README.md). There is output for every host but those that do not exist, which `--all` adds. The counts at the end @@ -93,7 +93,8 @@ takes as long as its slowest address, and they are printed in the order of tryin `--timeout-ms` then is per address on each route. Beyond that, the next paragraph holds for each address, and nothing is followed here either. HTTP/2 and HTTP/3 have no reason phrase, so there the `OK` is what that status is called. A redirect has its `Location` -at the end of the line, as in `301 Moved Permanently -> /elsewhere`, and the header fields are left out. +at the end of the line, as in `301 Moved Permanently -> /elsewhere`, and `--all-headers` adds the +header fields of each response below its line. **One request means one.** The next route is for when connecting failed. Once a request has gone out, the host has been asked, so that a timeout or a response cut short ends it there, as a failure. diff --git a/doc/README.md b/doc/README.md index 873164e..6588cd9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,7 +3,7 @@ One host, asked for its records over DNS over TLS. This is how the result looks in a terminal: ```sh -build/http-probe --dot --dns-server one.one.one.one cloudflare.com +build/http-probe --all-headers --dot --dns-server one.one.one.one cloudflare.com ``` ![The result for cloudflare.com: 301 over HTTP/3, its HTTPS record and response header fields, and the summary line, in colour](sample-output.png) diff --git a/src/main.cpp b/src/main.cpp index 05464a5..88dd2c6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -46,8 +46,8 @@ constexpr std::string_view usage = R"(usage: http-probe [options] HOST... constexpr std::string_view about = "Looks up the HTTPS records of each host (RFC 9460), and requests https://HOST/ once, the way " - "they say or over TCP on port 443 if they don't, printing the status and header fields of the " - "response. Redirects are reported and not followed, and nothing else on the host is requested."; + "they say or over TCP on port 443 if they don't, printing the status of the response on one " + "line. Redirects are reported and not followed, and nothing else on the host is requested."; constexpr std::string_view exit_code = "The exit code is 0 if every host that was connected to answered, whatever the status, and " @@ -59,6 +59,7 @@ struct Arguments std::vector files; bool help = false; bool all = false; + bool all_headers = false; bool color = false; int verbosity = 0; }; @@ -177,6 +178,8 @@ po::options_description describe_options(Arguments& arguments) opts("each-address", po::bool_switch(&config.each_address), "send a request to each address of a host and print the status line of each, instead of " "one request per host"); + opts("all-headers", po::bool_switch(&arguments.all_headers), + "also print the header fields of each response, after its status line"); opts("all", po::bool_switch(&arguments.all), "also print the hosts that do not exist, and those that --h3, --h2 or --h1 leave out"); opts("color", po::bool_switch(&arguments.color), @@ -305,7 +308,7 @@ int main(int argc, char* argv[]) return 0; } - auto& [config, files, help, all, color, verbosity] = arguments; + auto& [config, files, help, all, all_headers, color, verbosity] = arguments; config.logger = set_up_logging(verbosity, color); const bool own_dns_server = !config.dns_server.empty(); @@ -318,7 +321,8 @@ int main(int argc, char* argv[]) ++with_records; if (all || (result.outcome != Outcome::not_found && result.outcome != Outcome::left_out)) { - std::print("{}", http_probe::format_result(result, color_results)); + std::print("{}", http_probe::format_result( + result, {.color = color_results, .all_headers = all_headers})); std::ignore = std::fflush(stdout); // a run can take a while, and it is worth watching } }; diff --git a/src/output.cpp b/src/output.cpp index 0e66a86..d12160a 100644 --- a/src/output.cpp +++ b/src/output.cpp @@ -103,14 +103,26 @@ std::string record_text(const c_ares_async::https_record& record, bool color) return text; } +// +// The header fields of a response, a line each, \p indent deep. +// +std::string fields_text(const HeaderFields& fields, std::string_view indent, bool color) +{ + std::string text; + for (const auto& [name, value] : fields) + text += std::format("{}{}: {}\n", indent, colored(name, color_of::field, color), value); + return text; +} + // ================================================================================================= } // namespace -std::string format_result(const Result& result, bool color) +std::string format_result(const Result& result, const Style& style) { using Outcome = Result::Outcome; + const bool color = style.color; const auto host = colored(result.host, color_of::name, color); std::string text; switch (result.outcome) @@ -154,12 +166,16 @@ std::string format_result(const Result& result, bool color) for (const auto& address : result.addresses) text += std::format(" address: {}\n", address_text(address, color)); for (const auto& reply : result.replies) + { text += std::format(" address: {} {}\n", address_text(reply.address, color), reply_text(reply, color)); + if (style.all_headers) + text += fields_text(reply.fields, " ", color); + } for (const auto& note : result.notes) text += colored(std::format(" note: {}", note), color_of::note, color) + "\n"; - for (const auto& [name, value] : result.fields) - text += std::format(" {}: {}\n", colored(name, color_of::field, color), value); + if (style.all_headers) + text += fields_text(result.fields, " ", color); return text; } diff --git a/src/output.hpp b/src/output.hpp index 851fb48..f0d2cfd 100644 --- a/src/output.hpp +++ b/src/output.hpp @@ -18,8 +18,16 @@ namespace http_probe using Counts = std::map; -/// All there is to say about one host, a line each and with a line break at the end. -std::string format_result(const Result& result, bool color); +/// What to print of a result, and how. +struct Style +{ + bool color = false; + bool all_headers = false; ///< the header fields of each response, not only its status line +}; + +/// All there is to say about one host, a line each and with a line break at the end. A response +/// has a line of its own, and with Style::all_headers its header fields after that. +std::string format_result(const Result& result, const Style& style); /// How it went overall, without a line break. std::string format_summary(Counts counts, bool color); diff --git a/src/prober.cpp b/src/prober.cpp index c8b2880..505d724 100644 --- a/src/prober.cpp +++ b/src/prober.cpp @@ -46,6 +46,14 @@ std::string redirect_location(const anyhttp::client::Message& message) : std::string(); } +HeaderFields header_fields(const anyhttp::client::Message& message) +{ + HeaderFields fields; + for (const auto& field : message) + fields.emplace_back(field.name_string(), field.value()); + return fields; +} + // // The resolver does not write anywhere by itself. Its messages go where our own go. // @@ -465,8 +473,7 @@ capy::task Prober::probe(const std::string& host) auto spoken = route; spoken.protocol = response.protocol; result.route = spoken.to_string(); - for (const auto& field : response.message) - result.fields.emplace_back(field.name_string(), field.value()); + result.fields = header_fields(response.message); result.body_size = response.message.body().size(); result.duration = response.duration; co_return result; @@ -500,6 +507,7 @@ capy::task> Prober::ask_address(const std::vector(response.message.result_int()); reply.reason = response.message.reason(); reply.location = redirect_location(response.message); + reply.fields = header_fields(response.message); reply.error.clear(); co_return {std::error_code(), std::move(reply)}; } diff --git a/src/prober.hpp b/src/prober.hpp index fb520e8..018cd56 100644 --- a/src/prober.hpp +++ b/src/prober.hpp @@ -99,6 +99,9 @@ std::vector read_hosts(std::istream& list, std::vector /// Moves the IPv6 addresses to the front, each family keeping the order it was in. void prefer_ipv6(std::vector& addresses); +/// The header fields of a response, in the order they came. +using HeaderFields = std::vector>; + /// What came of the request to one address of a host, see Config::each_address. struct AddressReply { @@ -106,6 +109,7 @@ struct AddressReply int status = 0; ///< without \c error std::string reason; ///< without \c error: "OK", which HTTP/2 and HTTP/3 only know by the status std::string location; ///< with a 3xx status: the Location field as it came, see Result::location + HeaderFields fields; ///< without \c error std::string error; ///< what went wrong on the last route, if no response could be had }; @@ -130,7 +134,7 @@ struct Result int status = 0; ///< with \c answered std::string route; ///< with \c answered: what the response came over, as the server chose it - std::vector> fields; ///< with \c answered + HeaderFields fields; ///< with \c answered /// With \c answered and a 3xx status: the Location field as it came, neither resolved nor /// followed, and empty without one. std::string location; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 093757b..737e54e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -99,6 +99,13 @@ add_test(NAME http_probe_refuses_each_address_with_dns_only set_tests_properties(http_probe_refuses_each_address_with_dns_only PROPERTIES PASS_REGULAR_EXPRESSION "rules out --each-address") +# --all is a prefix of --all-headers, which program_options takes abbreviations of, and is still +# itself rather than ambiguous. +add_test(NAME http_probe_tells_all_from_all_headers + COMMAND http-probe --all --dns-server 127.0.0.1:1 localhost) +set_tests_properties(http_probe_tells_all_from_all_headers PROPERTIES + PASS_REGULAR_EXPRESSION "1 lookups failed") + # An empty list gets as far as having no hosts, and goes nowhere. add_test(NAME http_probe_takes_each_address_with_a_list COMMAND http-probe --each-address --file /dev/null) diff --git a/test/output_test.cpp b/test/output_test.cpp index 2137d14..2e9eeda 100644 --- a/test/output_test.cpp +++ b/test/output_test.cpp @@ -34,21 +34,66 @@ Result answered(int status = 200) TEST(Output, SaysWhatCameBack) { - EXPECT_EQ(format_result(answered(), false), + EXPECT_EQ(format_result(answered(), {}), + "example.com: 200 [h3 example.com:443] at 192.0.2.1, 5 bytes in 12ms\n" + " record: HTTPS 1 . alpn=h3 ipv4hint=192.0.2.1 ipv6hint=2001:db8::1\n"); +} + +TEST(Output, HasTheHeaderFieldsOnlyWithAllHeaders) +{ + auto result = answered(); + result.fields = {{"server", "test"}, {"content-type", "text/html"}}; + result.notes = {"some note"}; + + EXPECT_EQ(format_result(result, {.all_headers = true}), "example.com: 200 [h3 example.com:443] at 192.0.2.1, 5 bytes in 12ms\n" " record: HTTPS 1 . alpn=h3 ipv4hint=192.0.2.1 ipv6hint=2001:db8::1\n" - " server: test\n"); + " note: some note\n" + " server: test\n" + " content-type: text/html\n"); + EXPECT_THAT(format_result(result, {}), Not(HasSubstr("server"))); + + EXPECT_THAT(format_result(result, {.color = true, .all_headers = true}), + HasSubstr(" \x1b[1;34mserver\x1b[0m: test\n")); +} + +TEST(Output, HasTheHeaderFieldsOfEachAddressWithAllHeaders) +{ + const Result result{ + .host = "example.com", + .outcome = Outcome::failed, + .replies = {{.address = "2001:db8::1", .status = 200, .fields = {{"server", "a"}}}, + {.address = "192.0.2.1", .error = "[h2 x:443] connect: no"}, + {.address = "192.0.2.2", + .status = 301, + .location = "/b", + .fields = {{"server", "c"}, {"location", "/b"}}}}, + .notes = {"[h2 x:443] connect: no at 192.0.2.1"}}; + + EXPECT_EQ(format_result(result, {.all_headers = true}), + "example.com: failed\n" + " address: 2001:db8::1 200\n" + " server: a\n" + " address: 192.0.2.1 failed: [h2 x:443] connect: no\n" + " address: 192.0.2.2 301 -> /b\n" + " server: c\n" + " location: /b\n" + " note: [h2 x:443] connect: no at 192.0.2.1\n"); + EXPECT_EQ(format_result(result, {}), "example.com: failed\n" + " address: 2001:db8::1 200\n" + " address: 192.0.2.1 failed: [h2 x:443] connect: no\n" + " address: 192.0.2.2 301 -> /b\n" + " note: [h2 x:443] connect: no at 192.0.2.1\n"); } TEST(Output, SaysWhereARedirectPoints) { auto result = answered(301); result.location = "https://www.example.com/"; - EXPECT_EQ(format_result(result, false), + EXPECT_EQ(format_result(result, {}), "example.com: 301 [h3 example.com:443] at 192.0.2.1, 5 bytes in 12ms" " -> https://www.example.com/\n" - " record: HTTPS 1 . alpn=h3 ipv4hint=192.0.2.1 ipv6hint=2001:db8::1\n" - " server: test\n"); + " record: HTTPS 1 . alpn=h3 ipv4hint=192.0.2.1 ipv6hint=2001:db8::1\n"); } TEST(Output, SaysWhereARedirectPointsForEachAddress) @@ -59,9 +104,9 @@ TEST(Output, SaysWhereARedirectPointsForEachAddress) .replies = {{.address = "2001:db8::1", .status = 308, .location = "/elsewhere"}, {.address = "192.0.2.1", .status = 301, .reason = "Moved", .location = "/b"}}}; - EXPECT_EQ(format_result(result, false), "example.com: answered\n" - " address: 2001:db8::1 308 -> /elsewhere\n" - " address: 192.0.2.1 301 Moved -> /b\n"); + EXPECT_EQ(format_result(result, {}), "example.com: answered\n" + " address: 2001:db8::1 308 -> /elsewhere\n" + " address: 192.0.2.1 301 Moved -> /b\n"); } TEST(Output, SaysWhichAddressFailed) @@ -70,17 +115,17 @@ TEST(Output, SaysWhichAddressFailed) .outcome = Outcome::failed, .address = "2001:db8::1", .error = "[h3 example.com:443] timed out"}, - false), + {}), "example.com: failed at 2001:db8::1: [h3 example.com:443] timed out\n"); } TEST(Output, ColoursAnAddressByItsFamily) { auto result = answered(); - EXPECT_THAT(format_result(result, true), HasSubstr("at \x1b[35m192.0.2.1\x1b[0m,")); + EXPECT_THAT(format_result(result, {.color = true}), HasSubstr("at \x1b[35m192.0.2.1\x1b[0m,")); result.address = "2001:db8::1"; - EXPECT_THAT(format_result(result, true), HasSubstr("at \x1b[34m2001:db8::1\x1b[0m,")); + EXPECT_THAT(format_result(result, {.color = true}), HasSubstr("at \x1b[34m2001:db8::1\x1b[0m,")); } TEST(Output, SaysWhyThereIsNoResponse) @@ -89,19 +134,19 @@ TEST(Output, SaysWhyThereIsNoResponse) .outcome = Outcome::failed, .error = "[h3 example.com:443] timed out", .notes = {"some note"}}, - false), + {}), "example.com: failed: [h3 example.com:443] timed out\n note: some note\n"); - EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::out_of_reach}, false), + EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::out_of_reach}, {}), "example.com: out of reach\n"); - EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::not_found}, false), + EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::not_found}, {}), "example.com: not found\n"); - EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::lookup_failed}, false), + EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::lookup_failed}, {}), "example.com: lookup failed\n"); } TEST(Output, SaysThatAHostWasLeftOut) { - EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::left_out}, false), + EXPECT_EQ(format_result({.host = "example.com", .outcome = Outcome::left_out}, {}), "example.com: left out\n"); } @@ -119,12 +164,12 @@ TEST(Output, ListsTheAddressesOfAHostThatWasOnlyLookedUp) .records = {{.priority = 1, .target = ".", .params = {"alpn=h3"}}}, .addresses = {"2001:db8::1", "192.0.2.1"}}; - EXPECT_EQ(format_result(result, false), "example.com: resolved\n" - " record: HTTPS 1 . alpn=h3\n" - " address: 2001:db8::1\n" - " address: 192.0.2.1\n"); + EXPECT_EQ(format_result(result, {}), "example.com: resolved\n" + " record: HTTPS 1 . alpn=h3\n" + " address: 2001:db8::1\n" + " address: 192.0.2.1\n"); - const auto text = format_result(result, true); + const auto text = format_result(result, {.color = true}); EXPECT_THAT(text, HasSubstr("\x1b[32mresolved\x1b[0m")); EXPECT_THAT(text, HasSubstr("address: \x1b[34m2001:db8::1\x1b[0m")); EXPECT_THAT(text, HasSubstr("address: \x1b[35m192.0.2.1\x1b[0m")); @@ -147,13 +192,13 @@ TEST(Output, HasALinePerAddressWithARequestToEach) {.address = "192.0.2.1", .status = 299}, {.address = "192.0.2.2", .error = "[h2 x:443] connect: no"}}}; - EXPECT_EQ(format_result(result, false), "example.com: failed\n" - " record: HTTPS 1 . alpn=h2\n" - " address: 2001:db8::1 200 OK\n" - " address: 192.0.2.1 299\n" - " address: 192.0.2.2 failed: [h2 x:443] connect: no\n"); + EXPECT_EQ(format_result(result, {}), "example.com: failed\n" + " record: HTTPS 1 . alpn=h2\n" + " address: 2001:db8::1 200 OK\n" + " address: 192.0.2.1 299\n" + " address: 192.0.2.2 failed: [h2 x:443] connect: no\n"); - const auto text = format_result(result, true); + const auto text = format_result(result, {.color = true}); EXPECT_THAT(text, HasSubstr("address: \x1b[34m2001:db8::1\x1b[0m \x1b[32m200\x1b[0m OK")); EXPECT_THAT(text, HasSubstr("\x1b[31mfailed\x1b[0m: [h2 x:443]")); } @@ -164,13 +209,13 @@ TEST(Output, SaysThatAllAddressesAnswered) .outcome = Outcome::answered, .replies = {{.address = "192.0.2.1", .status = 301, .reason = "Moved"}}}; - EXPECT_EQ(format_result(result, false), "example.com: answered\n" - " address: 192.0.2.1 301 Moved\n"); + EXPECT_EQ(format_result(result, {}), "example.com: answered\n" + " address: 192.0.2.1 301 Moved\n"); } TEST(Output, ColoursTheWayCAresAsyncDoes) { - const auto text = format_result(answered(), true); + const auto text = format_result(answered(), {.color = true}); EXPECT_THAT(text, HasSubstr("\x1b[33mexample.com\x1b[0m:")) << "host names are yellow"; EXPECT_THAT(text, HasSubstr("ipv4hint=\x1b[35m192.0.2.1\x1b[0m")) << "IPv4 is magenta"; @@ -182,11 +227,11 @@ TEST(Output, LeavesTheCommasOfAnAddressListWithoutColour) auto result = answered(); result.records[0].params = {"ipv4hint=192.0.2.1,192.0.2.2", "ipv6hint=2001:db8::1,2001:db8::2"}; - const auto text = format_result(result, true); + const auto text = format_result(result, {.color = true}); EXPECT_THAT(text, HasSubstr("ipv4hint=\x1b[35m192.0.2.1\x1b[0m,\x1b[35m192.0.2.2\x1b[0m")); EXPECT_THAT(text, HasSubstr("ipv6hint=\x1b[34m2001:db8::1\x1b[0m,\x1b[34m2001:db8::2\x1b[0m")); - EXPECT_THAT(format_result(result, false), + EXPECT_THAT(format_result(result, {}), HasSubstr("ipv4hint=192.0.2.1,192.0.2.2 ipv6hint=2001:db8::1,2001:db8::2")); } @@ -195,16 +240,16 @@ TEST(Output, ColoursATargetAsAHostName) auto result = answered(); result.records[0].target = "svc.example.net"; - EXPECT_THAT(format_result(result, true), HasSubstr("\x1b[33msvc.example.net\x1b[0m")); - EXPECT_THAT(format_result(result, false), HasSubstr("HTTPS 1 svc.example.net alpn=h3")); + EXPECT_THAT(format_result(result, {.color = true}), HasSubstr("\x1b[33msvc.example.net\x1b[0m")); + EXPECT_THAT(format_result(result, {}), HasSubstr("HTTPS 1 svc.example.net alpn=h3")); } TEST(Output, ColoursTheStatusByItsClass) { - EXPECT_THAT(format_result(answered(204), true), HasSubstr("\x1b[32m204\x1b[0m")); - EXPECT_THAT(format_result(answered(301), true), HasSubstr("\x1b[36m301\x1b[0m")); - EXPECT_THAT(format_result(answered(404), true), HasSubstr("\x1b[31m404\x1b[0m")); - EXPECT_THAT(format_result(answered(503), true), HasSubstr("\x1b[31m503\x1b[0m")); + EXPECT_THAT(format_result(answered(204), {.color = true}), HasSubstr("\x1b[32m204\x1b[0m")); + EXPECT_THAT(format_result(answered(301), {.color = true}), HasSubstr("\x1b[36m301\x1b[0m")); + EXPECT_THAT(format_result(answered(404), {.color = true}), HasSubstr("\x1b[31m404\x1b[0m")); + EXPECT_THAT(format_result(answered(503), {.color = true}), HasSubstr("\x1b[31m503\x1b[0m")); } TEST(Output, HasNoColourWithoutColour) @@ -212,7 +257,7 @@ TEST(Output, HasNoColourWithoutColour) for (const auto& result : {answered(), Result{.host = "h", .outcome = Outcome::failed, .notes = {"n"}}, Result{.host = "h", .outcome = Outcome::lookup_failed}}) - EXPECT_THAT(format_result(result, false), Not(HasSubstr("\x1b"))); + EXPECT_THAT(format_result(result, {.all_headers = true}), Not(HasSubstr("\x1b"))); const Counts counts{{Outcome::answered, 1}, {Outcome::failed, 1}}; EXPECT_THAT(format_summary(counts, false), Not(HasSubstr("\x1b"))); diff --git a/test/prober_test.cpp b/test/prober_test.cpp index 5c3d065..d7e9bde 100644 --- a/test/prober_test.cpp +++ b/test/prober_test.cpp @@ -1397,6 +1397,7 @@ TEST_F(Probe, EachAddressSendsOneRequestToEachOfThem) EXPECT_THAT(reply.address, Not(IsEmpty())); EXPECT_EQ(reply.status, 200); EXPECT_EQ(reply.reason, "OK"); + EXPECT_THAT(reply.fields, Contains(Pair("content-type", "text/html"))); EXPECT_THAT(reply.error, IsEmpty()); } EXPECT_EQ(site.requests().size(), results[0].replies.size());