update_engine: call res_init and retry one extra time on unresolved host
libcurl error

Based on https://curl.haxx.se/docs/todo.html#updated_DNS_server_while_running:

"If /etc/resolv.conf gets updated while a program using libcurl is running, it
may cause name resolves to fail unless res_init() is called. We should
consider calling res_init() + retry once unconditionally on all name resolve
failures to mitigate against this."

This CL added following behavior:
On libcurl returns CURLE_COULDNT_RESOLVE_HOST error code:
1. we increase the max retry count by 1 for the first time it happens in the
lifetime of an LibcurlHttpFetcher object.
2. we call res_init unconditionally.

We also add UMA metrics to measure whether calling res_init helps
mitigate the unresolved host problem. WIP CL: https://chromium-review.googlesource.com/c/chromium/src/+/1698722

BUG=chromium:982813
TEST=FEATURES="test" emerge-kefka update_engine, tested on a device

Change-Id: Ia894eae93b3a0adbac1a831e657b75cba835dfa0
diff --git a/libcurl_http_fetcher_unittest.cc b/libcurl_http_fetcher_unittest.cc
index 88e48fa..7f00dae 100644
--- a/libcurl_http_fetcher_unittest.cc
+++ b/libcurl_http_fetcher_unittest.cc
@@ -43,6 +43,7 @@
   brillo::FakeMessageLoop loop_{nullptr};
   FakeHardware fake_hardware_;
   LibcurlHttpFetcher libcurl_fetcher_{nullptr, &fake_hardware_};
+  UnresolvedHostStateMachine state_machine_;
 };
 
 TEST_F(LibcurlHttpFetcherTest, GetEmptyHeaderValueTest) {
@@ -78,4 +79,60 @@
   EXPECT_EQ(header_value, actual_header_value);
 }
 
+TEST_F(LibcurlHttpFetcherTest, InvalidURLTest) {
+  int no_network_max_retries = 1;
+  libcurl_fetcher_.set_no_network_max_retries(no_network_max_retries);
+
+  libcurl_fetcher_.BeginTransfer("not-an-URL");
+  while (loop_.PendingTasks()) {
+    loop_.RunOnce(true);
+  }
+
+  EXPECT_EQ(libcurl_fetcher_.get_no_network_max_retries(),
+            no_network_max_retries);
+}
+
+TEST_F(LibcurlHttpFetcherTest, CouldntResolveHostTest) {
+  int no_network_max_retries = 1;
+  libcurl_fetcher_.set_no_network_max_retries(no_network_max_retries);
+
+  // This test actually sends request to internet but according to
+  // https://tools.ietf.org/html/rfc2606#section-2, .invalid domain names are
+  // reserved and sure to be invalid. Ideally we should mock libcurl or
+  // reorganize LibcurlHttpFetcher so the part that sends request can be mocked
+  // easily.
+  // TODO(xiaochu) Refactor LibcurlHttpFetcher (and its relates) so it's
+  // easier to mock the part that depends on internet connectivity.
+  libcurl_fetcher_.BeginTransfer("https://An-uNres0lvable-uRl.invalid");
+  while (loop_.PendingTasks()) {
+    loop_.RunOnce(true);
+  }
+
+  // If libcurl fails to resolve the name, we call res_init() to reload
+  // resolv.conf and retry exactly once more. See crbug.com/982813 for details.
+  EXPECT_EQ(libcurl_fetcher_.get_no_network_max_retries(),
+            no_network_max_retries + 1);
+}
+
+TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineRetryFailedTest) {
+  state_machine_.UpdateState(true);
+  state_machine_.UpdateState(true);
+  EXPECT_EQ(state_machine_.getState(),
+            UnresolvedHostStateMachine::State::kNotRetry);
+}
+
+TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineRetrySucceedTest) {
+  state_machine_.UpdateState(true);
+  state_machine_.UpdateState(false);
+  EXPECT_EQ(state_machine_.getState(),
+            UnresolvedHostStateMachine::State::kRetriedSuccess);
+}
+
+TEST_F(LibcurlHttpFetcherTest, HttpFetcherStateMachineNoRetryTest) {
+  state_machine_.UpdateState(false);
+  state_machine_.UpdateState(false);
+  EXPECT_EQ(state_machine_.getState(),
+            UnresolvedHostStateMachine::State::kInit);
+}
+
 }  // namespace chromeos_update_engine