Handle missing groups [resolvePrimaryGroups]

Split from issue 55210043.
diff --git a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
index f0c5626..a0bb712 100644
--- a/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
+++ b/src/com/google/enterprise/adaptor/ad/AdAdaptor.java
@@ -269,12 +269,20 @@
 
     private void resolvePrimaryGroups() {
       int nadds = 0;
+      int missing_groups = 0;
       for (AdEntity e : entities) {
         if (e.isGroup()) {
           continue;
         }
         AdEntity user = e;
         AdEntity primaryGroup = bySid.get(user.getPrimaryGroupSid());
+        if (primaryGroup == null) {
+          missing_groups++;
+          log.log(Level.FINER,
+              "Group {0} -- primary group for user {1} -- not found",
+              new Object[]{user.getPrimaryGroupSid(), user});
+          continue;
+        }
         if (!members.containsKey(primaryGroup)) {
           members.put(primaryGroup, new TreeSet<String>());
         }
@@ -283,6 +291,9 @@
         nadds++;
       }
       log.log(Level.FINE, "# primary groups: {0}", members.keySet().size());
+      if (missing_groups > 0) {
+        log.log(Level.FINE, "# missing primary groups: {0}", missing_groups);
+      }
       log.log(Level.FINE, "# users added to all primary groups: {0}", nadds);
     }
 
diff --git a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
index 225e2be..79d7574 100644
--- a/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/ad/AdAdaptorTest.java
@@ -214,6 +214,57 @@
   }
 
   @Test
+  public void testGroupCatalogReadFromReturnsUserMissingPrimaryGroup()
+      throws Exception {
+    AdAdaptor.GroupCatalog groupCatalog = new AdAdaptor.GroupCatalog(
+        defaultLocalizedStringMap(), "example", /*feedBuiltinGroups=*/ false);
+    MockLdapContext ldapContext = defaultMockLdapContext();
+    // add a user
+    String filter = "(|(&(objectClass=group)"
+        + "(groupType:1.2.840.113556.1.4.803:=2147483648))"
+        + "(&(objectClass=user)(objectCategory=person)))";
+    String searchDn = "DN_for_default_naming_context";
+    ldapContext.addSearchResult(filter, "cn", searchDn, "group_name")
+               .addSearchResult(filter, "objectSid;binary", searchDn,
+                                      // S-1-5-32-544: local Admin. group
+                   hexStringToByteArray("01020000000000052000000020020000"))
+               .addSearchResult(filter, "objectGUID;binary", searchDn,
+                   hexStringToByteArray("000102030405060708090a0b0e"))
+               .addSearchResult(filter, "primaryGroupId", searchDn, "users")
+               .addSearchResult(filter, "sAMAccountName", searchDn, "sam");
+
+    AdServer adServer = new AdServer("localhost", ldapContext);
+    adServer.initialize();
+
+    groupCatalog.readFrom(adServer);
+
+    final AdEntity goldenEntity = new AdEntity("S-1-5-32-544",
+        "cn=name\\ under,DN_for_default_naming_context", "users", "sam");
+    final Map<AdEntity, Set<String>> goldenMembers =
+        new HashMap<AdEntity, Set<String>>();
+    final Map<String, AdEntity> goldenSid = new HashMap<String, AdEntity>();
+    goldenSid.put("S-1-5-32-544", goldenEntity);
+    final Map<String, AdEntity> goldenDn = new HashMap<String, AdEntity>();
+    goldenDn.put(goldenEntity.getDn(), goldenEntity);
+    final Map<AdEntity, String> goldenDomain = new HashMap<AdEntity, String>();
+    goldenDomain.put(goldenEntity, "BUILTIN");
+
+    final AdAdaptor.GroupCatalog golden = new AdAdaptor.GroupCatalog(
+      defaultLocalizedStringMap(), "example.com", /*feedBuiltinGroups=*/ true,
+      /*entities*/ Sets.newHashSet(goldenEntity),
+      /*members*/ goldenMembers,
+      /*bySid*/ goldenSid,
+      /*byDn*/ goldenDn,
+      /*domain*/ goldenDomain);
+
+    assertTrue(golden.equals(groupCatalog));
+
+    // make sure readFrom call is idempotent
+    groupCatalog.readFrom(adServer);
+    assertTrue(golden.equals(groupCatalog));
+  }
+
+  @Test
   public void testGroupCatalogResolveForeignSecurityPrincipals()
       throws Exception {
     Map<String, String> strings = defaultLocalizedStringMap();