diff --git a/CMakeLists.txt b/CMakeLists.txt index 8747b36..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}) @@ -48,16 +50,16 @@ 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 - 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/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 745179e..737e54e 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) @@ -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());