shill: WiFi: Reset PendingService if hidden network times out

If a hidden service times out associate, it will likely have
no endpoints, since wpa_supplicant failed to attain a CurrentBSS.
If so, the service has no reference to a wifi_ device and thus
cannot call WiFi::DisconnectFrom() to reset pending_service_.
In this case, we must perform the disconnect here ourselves.

BUG=chromium:226688
TEST=Unit tests + manual: while connected, attempt a connection
to a non-existent hidden network.  After the "out-of-range"
error appears, the system should re-connect where it was
originally connected.

Change-Id: I1d3b6940f9b077b890189dea649f48405dd6a494
Reviewed-on: https://gerrit.chromium.org/gerrit/47366
Reviewed-by: Paul Stewart <pstew@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
Commit-Queue: Paul Stewart <pstew@chromium.org>
diff --git a/wifi.cc b/wifi.cc
index 3f1021d..e769d69 100644
--- a/wifi.cc
+++ b/wifi.cc
@@ -1337,6 +1337,17 @@
   CHECK(pending_service_);
   pending_service_->DisconnectWithFailure(
       Service::kFailureOutOfRange, &unused_error);
+
+  // A hidden service may have no endpoints, since wpa_supplicant
+  // failed to attain a CurrentBSS.  If so, the service has no
+  // reference to |this| device and cannot call WiFi::DisconnectFrom()
+  // to reset pending_service_.  In this case, we must perform the
+  // disconnect here ourselves.
+  if (pending_service_) {
+    CHECK(!pending_service_->HasEndpoints());
+    LOG(INFO) << "Hidden service was not found.";
+    DisconnectFrom(pending_service_);
+  }
 }
 
 void WiFi::StartReconnectTimer() {
diff --git a/wifi_unittest.cc b/wifi_unittest.cc
index 49f4cc2..4223d80 100644
--- a/wifi_unittest.cc
+++ b/wifi_unittest.cc
@@ -274,6 +274,9 @@
   void ThrowDBusError() {
     throw DBus::Error("SomeDBusType", "A handy message");
   }
+  void ResetPendingService() {
+    SetPendingService(NULL);
+  }
 
  protected:
   typedef scoped_refptr<MockWiFiService> MockWiFiServiceRefPtr;
@@ -1241,7 +1244,7 @@
   EXPECT_FALSE(GetPendingTimeout().IsCancelled());
 }
 
-TEST_F(WiFiMainTest, TimeoutPendingService) {
+TEST_F(WiFiMainTest, TimeoutPendingServiceWithEndpoints) {
   StartWiFi();
   const base::CancelableClosure &pending_timeout = GetPendingTimeout();
   EXPECT_TRUE(pending_timeout.IsCancelled());
@@ -1249,8 +1252,34 @@
       SetupConnectingService(DBus::Path(), NULL, NULL));
   EXPECT_FALSE(pending_timeout.IsCancelled());
   EXPECT_EQ(service, GetPendingService());
-  EXPECT_CALL(*service, DisconnectWithFailure(Service::kFailureOutOfRange, _));
+  // Simulate a service with a wifi_ reference calling DisconnectFrom().
+  EXPECT_CALL(*service, DisconnectWithFailure(Service::kFailureOutOfRange, _))
+      .WillOnce(InvokeWithoutArgs(this, &WiFiObjectTest::ResetPendingService));
+  EXPECT_CALL(*service, HasEndpoints()).Times(0);
+  // DisconnectFrom() should not be called directly from WiFi.
+  EXPECT_CALL(*service, SetState(Service::kStateIdle)).Times(0);
+  EXPECT_CALL(*GetSupplicantInterfaceProxy(), Disconnect()).Times(0);
   pending_timeout.callback().Run();
+  Mock::VerifyAndClearExpectations(service);
+}
+
+TEST_F(WiFiMainTest, TimeoutPendingServiceWithoutEndpoints) {
+  StartWiFi();
+  const base::CancelableClosure &pending_timeout = GetPendingTimeout();
+  EXPECT_TRUE(pending_timeout.IsCancelled());
+  MockWiFiServiceRefPtr service(
+      SetupConnectingService(DBus::Path(), NULL, NULL));
+  EXPECT_FALSE(pending_timeout.IsCancelled());
+  EXPECT_EQ(service, GetPendingService());
+  // We expect the service to get a disconnect call, but in this scenario
+  // the service does nothing.
+  EXPECT_CALL(*service, DisconnectWithFailure(Service::kFailureOutOfRange, _));
+  EXPECT_CALL(*service, HasEndpoints()).WillOnce(Return(false));
+  // DisconnectFrom() should be called directly from WiFi.
+  EXPECT_CALL(*service, SetState(Service::kStateIdle)).Times(AtLeast(1));
+  EXPECT_CALL(*GetSupplicantInterfaceProxy(), Disconnect());
+  pending_timeout.callback().Run();
+  EXPECT_EQ(NULL, GetPendingService().get());
 }
 
 TEST_F(WiFiMainTest, DisconnectInvalidService) {