Merge branch 'master' of https://code.google.com/p/plexi.fs into FixShareAcl2

Conflicts:
	src/com/google/enterprise/adaptor/fs/FsAdaptor.java
	test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
diff --git a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
index 972cc84..d8a6103 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -134,13 +134,15 @@
   /** The namespace applied to ACL Principals. */
   private String namespace;
 
+  /** The filesystem change monitor. */
+  private FsMonitor monitor;
+
   private AdaptorContext context;
   private Path rootPath;
   private boolean isDfsUnc;
   private DocId rootPathDocId;
   private FileDelegate delegate;
-  private FsMonitor monitor;
-  private ShareAcls lastPushedShareAcls = new ShareAcls(null, null);
+  private ShareAcls lastPushedShareAcls = null;
 
   public FsAdaptor() {
     // At the moment, we only support Windows.
@@ -152,6 +154,31 @@
     }
   }
 
+  @VisibleForTesting
+  FsAdaptor(FileDelegate delegate) {
+    this.delegate = delegate;
+  }
+
+  @VisibleForTesting
+  Set<String> getSupportedWindowsAccounts() {
+    return supportedWindowsAccounts;
+  }
+
+  @VisibleForTesting
+  String getBuiltinPrefix() {
+    return builtinPrefix;
+  }
+
+  @VisibleForTesting
+  String getNamespace() {
+    return namespace;
+  }
+
+  @VisibleForTesting
+  BlockingQueue<Path> getFsMonitorQueue() {
+    return monitor.getQueue();
+  }
+
   @Override
   public void initConfig(Config config) {
     config.addKey(CONFIG_SRC, null);
@@ -238,11 +265,16 @@
   @Override
   public void destroy() {
     delegate.destroy();
-    monitor.destroy();
-    monitor = null;
+    // TODO (bmj): The check for null monitor is strictly for the tests,
+    // some of which may not have fully initialized the adaptor.  Maybe
+    // look into handling this less obtrusively in the future.
+    if (monitor != null) {
+      monitor.destroy();
+      monitor = null;
+    }
   }
 
-  private ShareAcls getShareAcls() throws IOException {
+  private ShareAcls readShareAcls() throws IOException {
     Acl shareAcl;
     Acl dfsShareAcl;
 
@@ -305,14 +337,14 @@
     // The pusher does not support fragments in named resources.
     // Feed a DocId that is just the SHARE_ACL fragment to avoid
     // collisions with the root docid.
-    ShareAcls shareAcls = getShareAcls();
+    ShareAcls shareAcls = readShareAcls();
     Map<DocId, Acl> namedResources = new HashMap<DocId, Acl>();
-    if (forcePush || ((shareAcls.dfsShareAcl != null)
-        && !shareAcls.dfsShareAcl.equals(lastPushedShareAcls.dfsShareAcl))) {
+    if ((shareAcls.dfsShareAcl != null) && (forcePush ||
+        !shareAcls.dfsShareAcl.equals(lastPushedShareAcls.dfsShareAcl))) {
       namedResources.put(DFS_SHARE_ACL_DOCID, shareAcls.dfsShareAcl);
     }
-    if (forcePush || ((shareAcls.shareAcl != null)
-        && !shareAcls.shareAcl.equals(lastPushedShareAcls.shareAcl))) {
+    if ((shareAcls.shareAcl != null) && (forcePush ||
+        !shareAcls.shareAcl.equals(lastPushedShareAcls.shareAcl))) {
       namedResources.put(SHARE_ACL_DOCID, shareAcls.shareAcl);
     }
     if (namedResources.size() > 0) {
@@ -443,10 +475,10 @@
     // Populate the document content.
     if (docIsDirectory) {
       HtmlResponseWriter writer = createHtmlResponseWriter(resp);
-      writer.start(id, getPathName(doc));
+      writer.start(id, getFileName(doc));
       for (Path file : delegate.newDirectoryStream(doc)) {
         if (isSupportedPath(file)) {
-          writer.addLink(delegate.newDocId(file), getPathName(file));
+          writer.addLink(delegate.newDocId(file), getFileName(file));
         }
       }
       writer.finish();
@@ -483,12 +515,14 @@
   }
 
   @VisibleForTesting
-  String getPathName(Path file) {
+  String getFileName(Path file) {
     // NOTE: file.getFileName() fails for UNC paths. Use file.toFile() instead.
-    return file.toFile().getName();
+    String name = file.toFile().getName();
+    return name.isEmpty() ? file.getRoot().toString() : name;
   }
 
-  private boolean isSupportedPath(Path p) throws IOException {
+  @VisibleForTesting
+  boolean isSupportedPath(Path p) throws IOException {
     return delegate.isRegularFile(p) || delegate.isDirectory(p);
   }
 
@@ -496,7 +530,8 @@
    * Verifies that the file is a descendant of the root directory,
    * and that it, nor none of its ancestors, is hidden.
    */
-  private boolean isVisibleDescendantOfRoot(Path doc) throws IOException {
+  @VisibleForTesting
+  boolean isVisibleDescendantOfRoot(Path doc) throws IOException {
     for (Path file = doc; file != null; file = file.getParent()) {
       if (delegate.isHidden(file)) {
         if (doc.equals(file)) {
@@ -531,8 +566,8 @@
       Preconditions.checkNotNull(pusher, "the DocId pusher may not be null");
       Preconditions.checkArgument(maxFeedSize > 0,
           "the maxFeedSize must be greater than zero");
-      Preconditions.checkArgument(maxLatencyMillis > 0,
-          "the maxLatencyMillis must be greater than zero");
+      Preconditions.checkArgument(maxLatencyMillis >= 0,
+          "the maxLatencyMillis must be greater than or equal to zero");
       this.pusher = pusher;
       this.maxFeedSize = maxFeedSize;
       this.maxLatencyMillis = maxLatencyMillis;
diff --git a/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java b/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
index 3cf0f47..e0d50f3 100644
--- a/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
+++ b/src/com/google/enterprise/adaptor/fs/HtmlResponseWriter.java
@@ -15,6 +15,7 @@
 package com.google.enterprise.adaptor.fs;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
 import com.google.enterprise.adaptor.DocId;
 import com.google.enterprise.adaptor.DocIdEncoder;
 
@@ -213,10 +214,10 @@
   }
 
   private String computeLabel(String label, DocId doc) {
-    if (label == null || "".equals(label)) {
+    if (Strings.isNullOrEmpty(label)) {
       // Use the last part of the URL if an item doesn't have a title. The last
       // part of the URL will generally be a filename in this case.
-      String[] parts = doc.getUniqueId().split("/");
+      String[] parts = doc.getUniqueId().split("/", 0);
       label = parts[parts.length - 1];
     }
     return label;
diff --git a/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java b/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
index 199b568..b70e455 100644
--- a/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
+++ b/src/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViews.java
@@ -14,6 +14,7 @@
 
 package com.google.enterprise.adaptor.fs;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -67,12 +68,9 @@
  */
 class WindowsAclFileAttributeViews {
 
-  private static final Logger log = 
+  private static final Logger log =
       Logger.getLogger(WindowsAclFileAttributeViews.class.getName());
 
-  private static final Kernel32 KERNEL32 = Kernel32.INSTANCE;
-  private static final Advapi32 ADVAPI32 = Advapi32.INSTANCE; 
-
   /** This pattern parses a UNC path to get the host and share details. */
   private static final Pattern UNC_PATTERN =
       Pattern.compile("^\\\\\\\\([^\\\\]+)\\\\([^\\\\]+)");
@@ -88,7 +86,7 @@
       Collections.unmodifiableSet(Sets.newHashSet(SID_NAME_USE.SidTypeUser));
 
   /** Map of NT GENERIC permissions to NT FILE permissions. */
-  private static final Map<Integer, Integer> GENERIC_PERMS_MAP = 
+  private static final Map<Integer, Integer> GENERIC_PERMS_MAP =
       Collections.unmodifiableMap(new HashMap<Integer, Integer>() {
           {
             put(WinNT.GENERIC_READ, WinNT.FILE_GENERIC_READ);
@@ -142,6 +140,28 @@
           }
       });
 
+  private final Advapi32 advapi32;
+  private final Kernel32 kernel32;
+  private final Mpr mpr;
+  private final Netapi32Ex netapi32;
+  private final Shlwapi shlwapi;
+
+  /** Constructor used for production. */
+  public WindowsAclFileAttributeViews() {
+    this(Advapi32.INSTANCE, Kernel32.INSTANCE, Mpr.INSTANCE,
+         Netapi32Ex.INSTANCE,Shlwapi.INSTANCE);
+  }
+
+  /** Constructor used by the tests. */
+  @VisibleForTesting
+  WindowsAclFileAttributeViews(Advapi32 advapi32, Kernel32 kernel32,
+      Mpr mpr, Netapi32Ex netapi32, Shlwapi shlwapi) {
+    this.advapi32 = advapi32;
+    this.kernel32 = kernel32;
+    this.mpr = mpr;
+    this.netapi32 = netapi32;
+    this.shlwapi = shlwapi;
+  }
 
   /**
    * Returns a container for the direct and inherited ACLs for
@@ -152,7 +172,7 @@
    */
   public AclFileAttributeViews getAclViews(Path path) throws IOException {
     String pathname = path.toRealPath(LinkOption.NOFOLLOW_LINKS).toString();
-    WinNT.ACCESS_ACEStructure[] aces = getFileSecurity(pathname, 
+    WinNT.ACCESS_ACEStructure[] aces = getFileSecurity(pathname,
         WinNT.DACL_SECURITY_INFORMATION |
         WinNT.PROTECTED_DACL_SECURITY_INFORMATION |
         WinNT.UNPROTECTED_DACL_SECURITY_INFORMATION);
@@ -160,7 +180,7 @@
     ImmutableList.Builder<AclEntry> direct = ImmutableList.builder();
 
     for (WinNT.ACCESS_ACEStructure ace : aces) {
-      AclEntry aclEntry = newAclEntry(ace);      
+      AclEntry aclEntry = newAclEntry(ace);
       if (aclEntry != null) {
         if ((ace.AceFlags & WinNT.INHERITED_ACE) == WinNT.INHERITED_ACE) {
           inherited.add(aclEntry);
@@ -192,23 +212,22 @@
    */
   public AclFileAttributeView getShareAclView(Path path)
       throws IOException, UnsupportedOperationException {
-    if (Shlwapi.INSTANCE.PathIsUNC(path.toString())) {
+    if (shlwapi.PathIsUNC(path.toString())) {
       log.log(Level.FINEST, "Using a UNC path.");
       return getUncShareAclView(path.toString());
-    } else if (Shlwapi.INSTANCE.PathIsNetworkPath(path.toString())) {
+    } else if (shlwapi.PathIsNetworkPath(path.toString())) {
       log.log(Level.FINEST, "Using a mapped drive.");
-      // Call WNetGetUniversalNameW with the size needed for 
+      // Call WNetGetUniversalNameW with the size needed for
       // UNIVERSAL_NAME_INFO. If WNetGetUniversalNameW returns ERROR_MORE_DATA
       // that indicates that a larger buffer is needed. If this happens, make
       // a second call to WNetGetUniversalNameW with a buffer big enough.
-      Mpr mprlib = Mpr.INSTANCE;
       Memory buf = new Memory(1024);
       IntByReference bufSize = new IntByReference((int) buf.size());
-      int result = mprlib.WNetGetUniversalNameW(path.getRoot().toString(),
+      int result = mpr.WNetGetUniversalNameW(path.getRoot().toString(),
           Mpr.UNIVERSAL_NAME_INFO_LEVEL, buf, bufSize);
       if (result == WinNT.ERROR_MORE_DATA) {
         buf = new Memory(bufSize.getValue());
-        result = Mpr.INSTANCE.WNetGetUniversalNameW(path.getRoot().toString(),
+        result = mpr.WNetGetUniversalNameW(path.getRoot().toString(),
             Mpr.UNIVERSAL_NAME_INFO_LEVEL, buf, bufSize);
       }
       if (result != WinNT.NO_ERROR) {
@@ -239,12 +258,11 @@
         new Object[] { host, share });
     return getShareAclView(host, share);
   }
-  
+
   private AclFileAttributeView getShareAclView(String host, String share)
       throws IOException {
-    Netapi32Ex netapi32 = Netapi32Ex.INSTANCE;
     PointerByReference buf = new PointerByReference();
-    
+
     // Call NetShareGetInfo with a 502 to get the security descriptor of the
     // share. The security descriptor contains the Acl details for the share
     // that the adaptor needs.
@@ -299,7 +317,7 @@
    * @return AclEntry representing the ace, or {@code null} if a valid
    *         AclEntry could not be created from the ace.
    */
-  public static AclEntry newAclEntry(WinNT.ACCESS_ACEStructure ace) {
+  public AclEntry newAclEntry(WinNT.ACCESS_ACEStructure ace) {
     // Map the type.
     AclEntryType aclType = ACL_TYPE_MAP.get(ace.AceType);
     if (aclType == null) {
@@ -311,7 +329,7 @@
     // Map the user.
     Account account;
     try {
-      account = Advapi32Util.getAccountBySid(ace.getSID());
+      account = getAccountBySid(ace.getSID());
     } catch (Win32Exception e) {
       // Only the least significant 16-bits signifies the HR code.
       if ((e.getHR().intValue() & 0xFFFF) == WinError.ERROR_NONE_MAPPED) {
@@ -336,7 +354,7 @@
           new Object[] { accountName, accountType });
       return null;
     }
-    
+
     // Expand NT GENERIC_* permissions to their FILE_GENERIC_* equivalents.
     int aceMask = ace.Mask;
     for (Map.Entry<Integer, Integer> e : GENERIC_PERMS_MAP.entrySet()) {
@@ -352,7 +370,7 @@
         aclPerms.add(e.getValue());
       }
     }
-    
+
     // Map the flags.
     Set<AclEntryFlag> aclFlags = EnumSet.noneOf(AclEntryFlag.class);
     for (Map.Entry<Byte, AclEntryFlag> e : ACL_FLAGS_MAP.entrySet()) {
@@ -369,6 +387,11 @@
         .build();
   }
 
+  @VisibleForTesting
+  Account getAccountBySid(WinNT.PSID sid) throws Win32Exception {
+    return Advapi32Util.getAccountBySid(sid);
+  }
+
   // One-to-one corresponance to WinNT.SID_NAME_USE "enumeration".
   private static final List<String> SID_TYPE_NAMES = ImmutableList.of(
       "Unknown", "User", "Group", "Domain", "Alias", "Well-known Group",
@@ -408,7 +431,8 @@
     }
   }
 
-  private interface Mpr extends StdCallLibrary {
+  @VisibleForTesting
+  interface Mpr extends StdCallLibrary {
     Mpr INSTANCE = (Mpr) Native.loadLibrary("Mpr", Mpr.class,
         W32APIOptions.UNICODE_OPTIONS);
 
@@ -442,21 +466,21 @@
     WString wpath = new WString(pathname);
     IntByReference lengthNeeded = new IntByReference();
 
-    if (ADVAPI32.GetFileSecurity(wpath, daclType, null, 0, lengthNeeded)) {
+    if (advapi32.GetFileSecurity(wpath, daclType, null, 0, lengthNeeded)) {
       throw new RuntimeException("GetFileSecurity was expected to fail with "
           + "ERROR_INSUFFICIENT_BUFFER");
     }
 
-    int rc = KERNEL32.GetLastError();
+    int rc = kernel32.GetLastError();
     if (rc != W32Errors.ERROR_INSUFFICIENT_BUFFER) {
       throw new IOException("Failed GetFileSecurity", new Win32Exception(rc));
     }
 
     Memory memory = new Memory(lengthNeeded.getValue());
-    if (!ADVAPI32.GetFileSecurity(wpath, daclType, memory, (int) memory.size(),
+    if (!advapi32.GetFileSecurity(wpath, daclType, memory, (int) memory.size(),
                                   lengthNeeded)) {
       throw new IOException("Failed GetFileSecurity",
-          new Win32Exception(KERNEL32.GetLastError()));
+          new Win32Exception(kernel32.GetLastError()));
     }
 
     WinNT.SECURITY_DESCRIPTOR_RELATIVE securityDescriptor =
diff --git a/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java b/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
index 18b139b..07c106d 100644
--- a/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
+++ b/src/com/google/enterprise/adaptor/fs/WindowsFileDelegate.java
@@ -99,7 +99,7 @@
 
     ImmutableList.Builder<AclEntry> builder = ImmutableList.builder();
     for (WinNT.ACCESS_ACEStructure ace : dacl.getACEStructures()) {
-      AclEntry entry = WindowsAclFileAttributeViews.newAclEntry(ace);
+      AclEntry entry = aclViews.newAclEntry(ace);
       if (entry != null) {
         builder.add(entry);
       }
diff --git a/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java b/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java
new file mode 100644
index 0000000..5c38e25
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/AccumulatingDocIdPusher.java
@@ -0,0 +1,96 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.Principal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+class AccumulatingDocIdPusher extends UnsupportedDocIdPusher {
+  private List<Record> records = new ArrayList<Record>();
+  private List<Map<DocId, Acl>> namedResouces
+      = new ArrayList<Map<DocId, Acl>>();
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds)
+      throws InterruptedException {
+    return pushDocIds(docIds, null);
+  }
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds,
+                          ExceptionHandler handler)
+      throws InterruptedException {
+    List<Record> records = new ArrayList<Record>();
+    for (DocId docId : docIds) {
+      records.add(new Record.Builder(docId).build());
+    }
+    Record record = pushRecords(records, handler);
+    return record == null ? null : record.getDocId();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records)
+      throws InterruptedException {
+    return pushRecords(records, null);
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records,
+                            ExceptionHandler handler)
+      throws InterruptedException {
+    for (Record record : records) {
+      this.records.add(record);
+    }
+    return null;
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources)
+      throws InterruptedException {
+    return pushNamedResources(resources, null);
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources,
+                                  ExceptionHandler handler)
+      throws InterruptedException {
+    namedResouces.add(Collections.unmodifiableMap(
+        new TreeMap<DocId, Acl>(resources)));
+    return null;
+  }
+
+  public List<Record> getRecords() {
+    return Collections.unmodifiableList(records);
+  }
+
+  public List<Map<DocId, Acl>> getNamedResources() {
+    return Collections.unmodifiableList(namedResouces);
+  }
+
+  public void reset() {
+    records.clear();
+    namedResouces.clear();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
index 9eb2d16..efb317c 100644
--- a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
@@ -1,4 +1,4 @@
-// Copyright 2013 Google Inc. All Rights Reserved.
+// Copyright 2014 Google Inc. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,38 +14,965 @@
 
 package com.google.enterprise.adaptor.fs;
 
-import com.google.enterprise.adaptor.AccumulatingDocIdPusher;
+import static com.google.enterprise.adaptor.fs.AclView.user;
+import static com.google.enterprise.adaptor.fs.AclView.group;
+import static com.google.enterprise.adaptor.fs.AclView.GenericPermission.*;
 
 import static org.junit.Assert.*;
 
+import static java.nio.file.attribute.AclEntryFlag.*;
+import static java.nio.file.attribute.AclEntryPermission.*;
+import static java.nio.file.attribute.AclEntryType.*;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.Acl.InheritanceType;
+import com.google.enterprise.adaptor.AdaptorContext;
+import com.google.enterprise.adaptor.Config;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher.Record;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.UserPrincipal;
+
 import org.junit.*;
 import org.junit.rules.ExpectedException;
 
+import java.io.File;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.FileTime;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
 
 /** Test cases for {@link FsAdaptor}. */
 public class FsAdaptorTest {
+
+  static final String ROOT = "/";
+  static final Path rootPath = Paths.get(ROOT);
+  static final DocId shareAclDocId = new DocId("shareAcl");
+
+  private final Set<String> windowsAccounts = ImmutableSet.of(
+      "BUILTIN\\Administrators", "Everyone", "BUILTIN\\Users",
+      "BUILTIN\\Guest", "NT AUTHORITY\\INTERACTIVE",
+      "NT AUTHORITY\\Authenticated Users");
+  private final String builtinPrefix = "BUILTIN\\";
+  private final String namespace = "Default";
+
+  private AdaptorContext context = new MockAdaptorContext();
+  private AccumulatingDocIdPusher pusher =
+      (AccumulatingDocIdPusher) context.getDocIdPusher();
+  private Config config = context.getConfig();
+  private MockFile root = new MockFile(ROOT, true);
+  private MockFileDelegate delegate = new MockFileDelegate(root);
+  private FsAdaptor adaptor = new FsAdaptor(delegate);
+  private DocId rootDocId;
+
+  @Before
+  public void setUp() throws Exception {
+    rootDocId = delegate.newDocId(rootPath);
+    adaptor.initConfig(config);
+    config.overrideKey("filesystemadaptor.src", root.getPath());
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "0");
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    adaptor.destroy();
+  }
+
   @Rule
   public ExpectedException thrown = ExpectedException.none();
 
+  private Path getPath(String path) {
+    return rootPath.resolve(path);
+  }
+
+  private DocId getDocId(String path) throws IOException {
+    return delegate.newDocId(getPath(path));
+  }
+
   @Test
-  public void testGetPathName() throws Exception {
-    TestHelper.assumeOsIsWindows();
-    FsAdaptor adaptor = new FsAdaptor();
-    assertEquals("share", adaptor.getPathName(Paths.get("\\\\host/share/")));
-    assertEquals("folder2", 
-        adaptor.getPathName(Paths.get("C:/folder1/folder2/")));
+  public void testAdaptorStartupShutdown() throws Exception {
+    // Construction of Adaptor happened in setUp(), and
+    // destruction will happen in tearDown(), so the only
+    // thing left is to init the context.
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNoSourcePath() throws Exception {
+    config.overrideKey("filesystemadaptor.src", "");
+    thrown.expect(IOException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNonRootSourcePath() throws Exception {
+    root.addChildren(new MockFile("subdir", true));
+    config.overrideKey("filesystemadaptor.src", getPath("subdir").toString());
+    thrown.expect(IllegalStateException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitDfsUncActiveStorageUnc() throws Exception {
+    root.setDfsUncActiveStorageUnc(Paths.get("\\\\dfshost\\share"));
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitNonLinkDfsUncActiveStorageUnc() throws Exception {
+    root.setDfsUncActiveStorageUnc(rootPath);
+    thrown.expect(IOException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testAdaptorInitSupportedWindowsAccounts() throws Exception {
+    String accounts = "Everyone, BUILTIN\\Users, NT AUTH\\New Users";
+    Set<String> expected =
+        ImmutableSet.of("Everyone", "BUILTIN\\Users", "NT AUTH\\New Users");
+    config.overrideKey("filesystemadaptor.supportedAccounts", accounts);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getSupportedWindowsAccounts());
+  }
+
+  @Test
+  public void testAdaptorInitBuiltinGroupPrefix() throws Exception {
+    String expected = "TestPrefix";
+    config.overrideKey("filesystemadaptor.builtinGroupPrefix", expected);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getBuiltinPrefix());
+  }
+
+  @Test
+  public void testAdaptorInitNamespace() throws Exception {
+    String expected = "TestNamespace";
+    config.overrideKey("adaptor.namespace", expected);
+    adaptor.init(context);
+    assertEquals(expected, adaptor.getNamespace());
+  }
+
+  @Test
+  public void testGetFolderName() throws Exception {
+    assertEquals("share", adaptor.getFileName(Paths.get("\\\\host/share/")));
+    assertEquals("folder2",
+        adaptor.getFileName(Paths.get("C:/folder1/folder2/")));
+    assertEquals("folder2",
+        adaptor.getFileName(Paths.get("/folder1/folder2/")));
+    assertEquals("share", adaptor.getFileName(Paths.get("\\\\host/share")));
+    assertEquals("folder1",
+        adaptor.getFileName(Paths.get("/folder1")));
+    assertEquals(File.separator,  // Windows flips the '/' to '\'.
+        adaptor.getFileName(Paths.get("/")));
+    assertEquals("C:\\",
+        adaptor.getFileName(Paths.get("C:\\")));
+  }
+
+  @Test
+  public void testIsSupportedPath() throws Exception {
+    root.addChildren(new MockFile("foo"), new MockFile("bar", true),
+                     new MockFile("link").setIsRegularFile(false));
+    assertTrue(adaptor.isSupportedPath(rootPath));
+    assertTrue(adaptor.isSupportedPath(getPath("foo")));
+    assertTrue(adaptor.isSupportedPath(getPath("bar")));
+    assertFalse(adaptor.isSupportedPath(getPath("link")));
+  }
+
+  @Test
+  public void testIsVisibleDescendantOfRoot() throws Exception {
+    adaptor.init(context);
+    root.addChildren(new MockFile("foo"),
+        new MockFile("hidden.txt").setIsHidden(true),
+        new MockFile("dir1", true).addChildren(new MockFile("bar"),
+            new MockFile("hidden.pdf").setIsHidden(true)),
+        new MockFile("hidden.dir", true).setIsHidden(true).addChildren(
+            new MockFile("baz")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(rootPath));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("foo")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("dir1")));
+    assertTrue(adaptor.isVisibleDescendantOfRoot(getPath("dir1/bar")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.txt")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("dir1/hidden.pdf")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.dir")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(getPath("hidden.dir/baz")));
+    assertFalse(adaptor.isVisibleDescendantOfRoot(null));
+  }
+
+  @Test
+  public void testGetDocIds() throws Exception {
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed an acl for the share.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    Acl expected = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.AND_BOTH_PERMIT).build();
+    assertEquals(1, namedResources.size());
+    assertEquals(1, namedResources.get(0).size());
+    assertEquals(expected, namedResources.get(0).get(shareAclDocId));
+  }
+
+  @Test
+  public void testGetDocIdsDfs() throws Exception {
+    Path uncPath = Paths.get("\\\\dfshost\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed acls for the share and the dfsShare.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    DocId dfsShareAcl = new DocId("dfsShareAcl");
+    Acl expectedDfsShareAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.AND_BOTH_PERMIT).build();
+    Acl expectedShareAcl = new Acl.Builder(expectedDfsShareAcl)
+        .setInheritFrom(dfsShareAcl).build();
+    assertEquals(1, namedResources.size());
+    Map<DocId, Acl> acls = namedResources.get(0);
+    assertEquals(2, acls.size());
+    assertEquals(expectedDfsShareAcl, acls.get(dfsShareAcl));
+    assertEquals(expectedShareAcl, acls.get(shareAclDocId));
+  }
+
+  @Test
+  public void testGetDocIdsBrokenDfs() throws Exception {
+    Path uncPath = Paths.get("\\\\dfshost\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    adaptor.init(context);
+
+    // Now make the active storage disappear.
+    root.setDfsUncActiveStorageUnc(null);
+    thrown.expect(IOException.class);
+    adaptor.getDocIds(pusher);
+  }
+
+  @Test
+  public void testGetDocContentUnsupportedPath() throws Exception {
+    root.addChildren(new MockFile("unsupported").setIsRegularFile(false));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("unsupported")), response);
+    assertTrue(response.notFound);
+  }
+
+  /*
+   * This test was invalidated when the check for isSupportedPath()
+   * was moved above the check for bad DocIds in the beginning of
+   * FsAdaptor.getDocumentContent().  This test still passes, but
+   * the notFound response is coming from the earlier check for
+   * isSupportedPath(), not from the bad DocId.
+   */
+  @Test
+  public void testGetDocContentBadDocId() throws Exception {
+    root.addChildren(new MockFile("badfile"));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    // The requested DocId is missing the root component of the path.
+    adaptor.getDocContent(new MockRequest(new DocId("badfile")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentHiddenFile() throws Exception {
+    root.addChildren(new MockFile("hidden.txt").setIsHidden(true));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("hidden.txt")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentHiddenDirectory() throws Exception {
+    root.addChildren(new MockFile("hidden.dir", true).setIsHidden(true));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId("hidden.dir")), response);
+    assertTrue(response.notFound);
+  }
+
+  @Test
+  public void testGetDocContentRegularFile() throws Exception {
+    String fname = "test.html";
+    Date modifyDate = new Date(30000);
+    FileTime modifyTime = FileTime.fromMillis(modifyDate.getTime());
+    String content = "<html><title>Hello World</title></html>";
+    root.addChildren(new MockFile(fname).setLastModifiedTime(modifyTime)
+        .setFileContents(content).setContentType("text/html"));
+    adaptor.init(context);
+
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId(fname)), response);
+    assertFalse(response.notFound);
+    assertEquals(modifyDate, response.lastModified);
+    assertEquals(getPath(fname).toUri(), response.displayUrl);
+    assertEquals("text/html", response.contentType);
+    assertEquals(content, response.content.toString("UTF-8"));
+    // TODO: check metadata.
+    assertNotNull(response.metadata.get("Creation Time"));
+    // ACL checked in other tests.
+  }
+
+  @Test
+  public void testGetDocContentRootAcl() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritFrom(shareAclDocId)
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES).build();
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(rootPath));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertEquals(expectedAcl, response.acl);
+  }
+
+  @Test
+  public void testGetDocContentEmptyAcl() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritFrom(rootDocId, "childFilesAcl")
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(MockFile.EMPTY_ACLVIEW, null, expectedAcl);
+  }
+
+  @Test
+  public void testGetDocContentDirectAcl() throws Exception {
+    AclFileAttributeView aclView = new AclView((user("joe")
+        .type(ALLOW).perms(GENERIC_READ).build()));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("joe")))
+        .setInheritFrom(rootDocId, "childFilesAcl")
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(aclView, null, expectedAcl);
+  }
+
+  @Test
+  public void testGetDocContentNoInheritAcl() throws Exception {
+    AclFileAttributeView aclView = new AclView((user("joe")
+        .type(ALLOW).perms(GENERIC_READ).build()));
+    // Should inherit from the share, not the parent.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("joe")))
+        .setInheritFrom(shareAclDocId)
+        .setInheritanceType(InheritanceType.LEAF_NODE).build();
+    testFileAcl(aclView, MockFile.EMPTY_ACLVIEW, expectedAcl);
+  }
+
+  private void testFileAcl(AclFileAttributeView aclView,
+      AclFileAttributeView inheritAclView, Acl expectedAcl) throws Exception {
+    String fname = "acltest";
+    root.addChildren(new MockFile(fname).setAclView(aclView)
+                     .setInheritedAclView(inheritAclView));
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(new MockRequest(getDocId(fname)), response);
+    assertEquals(expectedAcl, response.acl);
+  }
+
+  /** Test that LastAccessTime is restored after reading the file. */
+  @Test
+  public void testPreserveFileLastAccessTime() throws Exception {
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return super.newInputStream();
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception opening the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException1() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          throw new IOException("newInputStream");
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception reading the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException2() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return new FilterInputStream(super.newInputStream()) {
+              @Override
+              public int read(byte[] b, int off, int len) throws IOException {
+                throw new IOException("read");
+              }
+          };
+        }
+      });
+  }
+
+  /** Test LastAccessTime is restored even if exception closing the file. */
+  @Test
+  public void testPreserveFileLastAccessTimeException3() throws Exception {
+    thrown.expect(IOException.class);
+    testPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return new FilterInputStream(super.newInputStream()) {
+              @Override
+              public void close() throws IOException {
+                throw new IOException("close");
+              }
+          };
+        }
+      });
+  }
+
+  /** Test that failure to restore LastAccessTime is not fatal. */
+  @Test
+  public void testPreserveFileLastAccessTimeException4() throws Exception {
+    testNoPreserveFileLastAccessTime(new MockFile("test") {
+        @Override
+        InputStream newInputStream() throws IOException {
+          setLastAccessTime(FileTime.fromMillis(
+              getLastAccessTime().toMillis() + 1000));
+          return super.newInputStream();
+        }
+        @Override
+        MockFile setLastAccessTime(FileTime accessTime) throws IOException {
+          if (MockFile.DEFAULT_FILETIME.equals(getLastAccessTime())) {
+            // Let the above setting from newInputStream go through.
+            return super.setLastAccessTime(accessTime);
+          } else {
+            // But fail the attempt to restore from FsAdaptor.
+            throw new IOException("Restore LastAccessTime");
+          }
+        }
+      });
+  }
+
+  private void testPreserveFileLastAccessTime(MockFile file) throws Exception {
+    testFileLastAccessTime(file, true);
+  }
+
+  private void testNoPreserveFileLastAccessTime(MockFile file)
+      throws Exception {
+    testFileLastAccessTime(file, false);
+  }
+
+  private void testFileLastAccessTime(MockFile file, boolean isPreserved)
+      throws Exception {
+    String contents = "Test contents";
+    file.setFileContents(contents);
+    root.addChildren(file);
+    adaptor.init(context);
+    MockResponse response = new MockResponse();
+    FileTime lastAccess = file.getLastAccessTime();
+    adaptor.getDocContent(new MockRequest(getDocId(file.getName())), response);
+    // Verify we indeed accessed the file
+    assertEquals(contents, response.content.toString("UTF-8"));
+    if (isPreserved) {
+      assertEquals(lastAccess, file.getLastAccessTime());
+    } else {
+      assertFalse(lastAccess.equals(file.getLastAccessTime()));
+    }
+  }
+
+  @Test
+  public void testGetDocContentRoot() throws Exception {
+    testGetDocContentDirectory(rootPath, rootPath.toString());
+    // ACLs checked in other tests.
+  }
+
+  @Test
+  public void testGetDocContentDirectory() throws Exception {
+    String fname = "test.dir";
+    root.addChildren(new MockFile(fname, true));
+    testGetDocContentDirectory(getPath(fname), fname);
+    // ACLs checked in other tests.
+  }
+
+  private void testGetDocContentDirectory(Path path, String label)
+      throws Exception {
+    MockFile dir = delegate.getFile(path);
+    FileTime modifyTime = dir.getLastModifiedTime();
+    Date modifyDate = new Date(modifyTime.toMillis());
+    dir.addChildren(new MockFile("test.txt"), new MockFile("subdir", true));
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(path));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertFalse(response.notFound);
+    assertEquals(modifyDate, response.lastModified);
+    assertEquals(path.toUri(), response.displayUrl);
+    assertEquals("text/html; charset=UTF-8", response.contentType);
+    String expectedContent = "<!DOCTYPE html>\n<html><head><title>Folder "
+        + label + "</title></head><body><h1>Folder " + label + "</h1>"
+        + "<li><a href=\"subdir/\">subdir</a></li>"
+        + "<li><a href=\"test.txt\">test.txt</a></li></body></html>";
+    assertEquals(expectedContent, response.content.toString("UTF-8"));
+    assertNotNull(response.metadata.get("Creation Time"));
+  }
+
+  @Test
+  public void testGetDocContentDefaultRootAcls() throws Exception {
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(new GroupPrincipal("Everyone")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentInheritOnlyRootAcls() throws Exception {
+    AclFileAttributeView inheritOnlyAclView = new AclView(
+        user("Longfellow Deeds").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, INHERIT_ONLY),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(inheritOnlyAclView);
+
+    // The root ACL should only include Administrators, not Mr. Deeds.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // But the childrens' inherited ACLs should include Mr. Deeds
+    Acl expectedInheritableAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Longfellow Deeds")))
+        .build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedInheritableAcl,
+        "allFilesAcl", expectedInheritableAcl,
+        "childFoldersAcl", expectedInheritableAcl,
+        "childFilesAcl", expectedInheritableAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoPropagateRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Barren von Dink").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, NO_PROPAGATE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    // The root ACL should include both Administrators and the Barren.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Barren von Dink")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // The direct childrens' inherited ACLs should include both the
+    // Administrators and the Barren, but grandchildren should not
+    // inherit the Barren's NO_PROPAGATE permission.
+    Acl expectedNonChildAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedNonChildAcl,
+        "allFilesAcl", expectedNonChildAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFilesOnlyRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("For Your Files Only").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("For Your Files Only")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // Folders shouldn't include the file-only permissions.
+    Acl expectedFolderAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFolderAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedFolderAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFoldersOnlyRootAcls() throws Exception {
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Fluff 'n Folder").type(ALLOW).perms(GENERIC_READ)
+            .flags(DIRECTORY_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.setAclView(noPropagateAclView);
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Fluff 'n Folder")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    // Files shouldn't include the folder-only permissions.
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(rootPath, expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentDefaultDirectoryAcls() throws Exception {
+    String name = "subdir";
+    root.addChildren(new MockFile(name, true));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    Acl expectedFoldersAcl = new Acl.Builder(expectedAcl)
+        .setInheritFrom(rootDocId, "allFoldersAcl").build();
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setInheritFrom(rootDocId, "allFilesAcl").build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFoldersAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedFoldersAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoInheritDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView orphanAclView = new AclView(user("Annie").type(ALLOW)
+        .perms(GENERIC_READ).flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(orphanAclView)
+        .setInheritedAclView(MockFile.EMPTY_ACLVIEW));
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(new UserPrincipal("Annie")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(shareAclDocId).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedAcl,
+        "allFilesAcl", expectedAcl,
+        "childFoldersAcl", expectedAcl,
+        "childFilesAcl", expectedAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentInheritOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView inheritOnlyAclView = new AclView(
+        user("Longfellow Deeds").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, INHERIT_ONLY),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(inheritOnlyAclView));
+
+    // The root ACL should only include Administrators, not Mr. Deeds.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // But the childrens' inherited ACLs should include Mr. Deeds
+    Acl expectedFoldersAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Longfellow Deeds")))
+        .setInheritFrom(rootDocId, "allFoldersAcl").build();
+    Acl expectedFilesAcl = new Acl.Builder(expectedFoldersAcl)
+        .setInheritFrom(rootDocId, "allFilesAcl").build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", expectedFoldersAcl,
+        "allFilesAcl", expectedFilesAcl,
+        "childFoldersAcl", expectedFoldersAcl,
+        "childFilesAcl", expectedFilesAcl);
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentNoPropagateDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView noPropagateAclView = new AclView(
+        user("Barren von Dink").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT, NO_PROPAGATE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(noPropagateAclView));
+
+    // The root ACL should include both Administrators and the Barren.
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Barren von Dink")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // The direct childrens' inherited ACLs should include both the
+    // Administrators and the Barren, but grandchildren should not
+    // inherit the Barren's NO_PROPAGATE permission.
+    Acl expectedNonChildAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedNonChildAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedNonChildAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFilesOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView filesOnlyAclView = new AclView(
+        user("For Your Files Only").type(ALLOW).perms(GENERIC_READ)
+            .flags(FILE_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(filesOnlyAclView));
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("For Your Files Only")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // Folders shouldn't include the file-only permissions.
+    Acl expectedFolderAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedFolderAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedFolderAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  @Test
+  public void testGetDocContentFoldersOnlyDirectoryAcls() throws Exception {
+    String name = "subdir";
+    AclFileAttributeView foldersOnlyAclView = new AclView(
+        user("Fluff 'n Folder").type(ALLOW).perms(GENERIC_READ)
+            .flags(DIRECTORY_INHERIT),
+        group("Administrators").type(ALLOW).perms(GENERIC_ALL)
+            .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    root.addChildren(new MockFile(name, true).setAclView(foldersOnlyAclView));
+
+    Acl expectedAcl = new Acl.Builder().setEverythingCaseInsensitive()
+        .setPermitUsers(Collections.singleton(
+            new UserPrincipal("Fluff 'n Folder")))
+        .setPermitGroups(Collections.singleton(
+            new GroupPrincipal("Administrators")))
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES)
+        .setInheritFrom(rootDocId, "childFoldersAcl").build();
+    // Files shouldn't include the folder-only permissions.
+    Acl expectedFilesAcl = new Acl.Builder(expectedAcl)
+        .setPermitUsers(Collections.<UserPrincipal>emptySet()).build();
+    Map<String, Acl> expectedResources = ImmutableMap.of(
+        "allFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "allFilesAcl", new Acl.Builder(expectedFilesAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build(),
+        "childFoldersAcl", new Acl.Builder(expectedAcl)
+            .setInheritFrom(rootDocId, "allFoldersAcl").build(),
+        "childFilesAcl", new Acl.Builder(expectedFilesAcl)
+            .setInheritFrom(rootDocId, "allFilesAcl").build());
+    testGetDocContentAcls(getPath(name), expectedAcl, expectedResources);
+  }
+
+  private void testGetDocContentAcls(Path path, Acl expectedAcl,
+      Map<String, Acl> expectedAclResources) throws Exception {
+    adaptor.init(context);
+    MockRequest request = new MockRequest(delegate.newDocId(path));
+    MockResponse response = new MockResponse();
+    adaptor.getDocContent(request, response);
+    assertEquals(expectedAcl, response.acl);
+    assertEquals(expectedAclResources, response.namedResources);
+  }
+
+  @Test
+  public void testMonitorZeroFeedSize() throws Exception {
+    config.overrideKey("feed.maxUrls", "0");
+    thrown.expect(IllegalArgumentException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testMonitorZeroLatency() throws Exception {
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "0");
+    adaptor.init(context);
+  }
+
+
+  @Test
+  public void testMonitorSubZeroLatency() throws Exception {
+    config.overrideKey("adaptor.incrementalPollPeriodSecs", "-1");
+    thrown.expect(IllegalArgumentException.class);
+    adaptor.init(context);
+  }
+
+  @Test
+  public void testMonitorUnsupportedPath() throws Exception {
+    root.addChildren(new MockFile("unsuppored").setIsRegularFile(false));
+    adaptor.init(context);
+    BlockingQueue<Path> queue = adaptor.getFsMonitorQueue();
+    queue.add(getPath("unsupported"));
+    Thread.sleep(100L);  // Allow FsMonitor to drain the queue.
+    // Verify it has been processed, but not been fed.
+    assertEquals(0, queue.size());
+    assertEquals(0, pusher.getRecords().size());
+    assertEquals(0, pusher.getNamedResources().size());
+  }
+
+  @Test
+  public void testMonitorOneFile() throws Exception {
+    String name = "test";
+    root.addChildren(new MockFile(name));
+    adaptor.init(context);
+    testMonitor(name);
+  }
+
+  @Test
+  public void testMonitorOneDirectory() throws Exception {
+    String name = "test.dir";
+    root.addChildren(new MockFile(name, true));
+    adaptor.init(context);
+    testMonitor(name);
+  }
+
+  @Test
+  public void testMonitorMultipleItems() throws Exception {
+    String dirname = "subdir";
+    String fname = "test.txt";
+    root.addChildren(new MockFile(dirname, true), new MockFile(fname));
+    adaptor.init(context);
+    testMonitor(dirname, fname);
+  }
+
+  @Test
+  public void testMonitorMultipleBatches() throws Exception {
+    String dirname = "subdir";
+    String fname = "test.txt";
+    root.addChildren(new MockFile(dirname, true), new MockFile(fname));
+    adaptor.init(context);
+    testMonitor(dirname);
+    // Make sure the previous batch does not get fed again.
+    pusher.reset();
+    testMonitor(fname);
+  }
+
+  private void testMonitor(String... names) throws Exception {
+    BlockingQueue<Path> queue = adaptor.getFsMonitorQueue();
+    for (String name : names) {
+      queue.add(getPath(name));
+    }
+    Thread.sleep(100L); // Allow FsMonitor to drain the queue.
+    assertEquals(0, queue.size());
+    assertEquals(0, pusher.getNamedResources().size());
+    Set<Record> expected = Sets.newHashSet();
+    for (String name : names) {
+      expected.add(new Record.Builder(getDocId(name))
+                   .setCrawlImmediately(true).build());
+    }
+    assertEquals(expected, Sets.newHashSet(pusher.getRecords()));
   }
 
   @Test
   public void testIncrementalShareAcls() throws Exception {
-    FsAdaptor adaptor = new FsAdaptor();
-    AccumulatingDocIdPusher pusher = new AccumulatingDocIdPusher();
+    adaptor.init(context);
+
+    AclFileAttributeView aclView = new AclView(
+        user("joe").type(ALLOW).perms(GENERIC_READ)
+        .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    Acl acl =
+        newBuilder(aclView).getAcl().setInheritanceType(
+        InheritanceType.AND_BOTH_PERMIT).build();
+    root.setShareAclView(aclView);
+
     adaptor.getDocIds(pusher);
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    assertEquals(1, namedResources.size());
+    assertEquals(acl, namedResources.get(0).get(shareAclDocId));
+
+    // Clear the pusher and call getModifiedDocIds. Nothing should be
+    // pushed since the share Acl has not changed.
+    pusher.reset();
+    adaptor.getModifiedDocIds(pusher);
+    namedResources = pusher.getNamedResources();
+    assertEquals(0, namedResources.size());
+
+    // Change the share Acl and confirm that that the share Acl is pushed.
+    aclView = new AclView(user("mary").type(ALLOW).perms(GENERIC_READ)
+        .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+    acl = newBuilder(aclView).getAcl().setInheritanceType(
+        InheritanceType.AND_BOTH_PERMIT).build();
+    root.setShareAclView(aclView);
 
     adaptor.getModifiedDocIds(pusher);
-    assertEquals("share", adaptor.getPathName(Paths.get("\\\\host/share/")));
-    assertEquals("folder2", 
-        adaptor.getPathName(Paths.get("C:/folder1/folder2/")));
+    namedResources = pusher.getNamedResources();
+    assertEquals(1, namedResources.size());
+    assertEquals(acl, namedResources.get(0).get(shareAclDocId));
+  }
+
+  /** Returns an AclBuilder for the AclFileAttributeView. */
+  private AclBuilder newBuilder(AclFileAttributeView aclView) {
+    return new AclBuilder(Paths.get("foo", "bar"),
+        aclView, windowsAccounts, builtinPrefix, namespace);
   }
 }
diff --git a/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java b/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java
new file mode 100644
index 0000000..5749306
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockAdaptorContext.java
@@ -0,0 +1,110 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.AdaptorContext;
+import com.google.enterprise.adaptor.AuthnAuthority;
+import com.google.enterprise.adaptor.AuthzAuthority;
+import com.google.enterprise.adaptor.Config;
+import com.google.enterprise.adaptor.DocIdEncoder;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.PollingIncrementalLister;
+import com.google.enterprise.adaptor.SensitiveValueDecoder;
+import com.google.enterprise.adaptor.Session;
+import com.google.enterprise.adaptor.StatusSource;
+
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * Mock of {@link AdaptorContext}.
+ */
+class MockAdaptorContext implements AdaptorContext {
+  private final Config config = new Config();
+  private final DocIdPusher docIdPusher = new AccumulatingDocIdPusher();
+  private final DocIdEncoder docIdEncoder = new MockDocIdCodec();
+
+  @Override
+  public Config getConfig() {
+    return config;
+  }
+
+  @Override
+  public DocIdPusher getDocIdPusher() {
+    return docIdPusher;
+  }
+
+  @Override
+  public DocIdEncoder getDocIdEncoder() {
+    return docIdEncoder;
+  }
+
+  @Override
+  public void addStatusSource(StatusSource source) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setGetDocIdsFullErrorHandler(ExceptionHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ExceptionHandler getGetDocIdsFullErrorHandler() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setGetDocIdsIncrementalErrorHandler(
+      ExceptionHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public ExceptionHandler getGetDocIdsIncrementalErrorHandler() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public SensitiveValueDecoder getSensitiveValueDecoder() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public HttpContext createHttpContext(String path, HttpHandler handler) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Session getUserSession(HttpExchange ex, boolean create) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setPollingIncrementalLister(PollingIncrementalLister lister) {
+  }
+
+  @Override
+  public void setAuthnAuthority(AuthnAuthority authnAuthority) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setAuthzAuthority(AuthzAuthority authzAuthority) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java b/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java
new file mode 100644
index 0000000..e669083
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockDocIdCodec.java
@@ -0,0 +1,37 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdEncoder;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Mock of {@link DocIdCodec}.
+ */
+class MockDocIdCodec implements DocIdEncoder {
+  @Override
+  public URI encodeDocId(DocId docId) {
+    try {
+      String id = docId.getUniqueId();
+      return new URI("http", "localhost",
+          id.startsWith("/") ? id : "/" + id, null);
+    } catch (URISyntaxException ex) {
+      throw new AssertionError();
+    }
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFile.java b/test/com/google/enterprise/adaptor/fs/MockFile.java
new file mode 100644
index 0000000..eb97926
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFile.java
@@ -0,0 +1,399 @@
+// Copyright 2009 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import static com.google.enterprise.adaptor.fs.AclView.user;
+import static com.google.enterprise.adaptor.fs.AclView.group;
+
+import static java.nio.file.attribute.AclEntryFlag.*;
+import static java.nio.file.attribute.AclEntryPermission.*;
+import static java.nio.file.attribute.AclEntryType.*;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+class MockFile {
+  static final String SEPARATOR = "/";
+  static final FileTime DEFAULT_FILETIME = FileTime.fromMillis(10000);
+  static final AclFileAttributeView FULL_ACCESS_ACLVIEW =
+      new AclView(group("Everyone").type(ALLOW)
+          .perms(READ_DATA, READ_ATTRIBUTES, READ_NAMED_ATTRS, READ_ACL)
+          .flags(FILE_INHERIT, DIRECTORY_INHERIT));
+  static final AclFileAttributeView EMPTY_ACLVIEW = new AclView();
+
+  private MockFile parent;
+  private String name;
+  private boolean isHidden = false;
+  private boolean isRegularFile;
+  private boolean isDirectory;
+  private List<MockFile> directoryContents;
+  private Path dfsUncActiveStorageUnc;
+  private AclFileAttributeView dfsShareAclView;
+  private AclFileAttributeView shareAclView;
+  private AclFileAttributeView aclView;
+  private AclFileAttributeView inheritedAclView;
+  private FileTime creationTime = DEFAULT_FILETIME;
+  private FileTime lastModifiedTime = DEFAULT_FILETIME;
+  private FileTime lastAccessTime = DEFAULT_FILETIME;
+  private String contentType;
+  private byte[] fileContents;
+
+  /**
+   * Create a regular file with the specified {@code name}.
+   *
+   * @param name the name of the file
+   */
+  MockFile(String name) {
+    this(name, false);
+  }
+
+  /**
+   * Create a file or directory with the specified {@code name}.
+   *
+   * @param name the name of the file or directory
+   * @param isDirectory true if this is a directory, false if regular file
+   */
+  MockFile(String name, boolean isDirectory) {
+    Preconditions.checkNotNull(name, "name cannot be null");
+    this.name = name;
+    this.isRegularFile = !isDirectory;
+    this.isDirectory = isDirectory;
+    if (isDirectory) {
+      directoryContents = new ArrayList<MockFile>();
+    } else {
+      setFileContents("Contents of " + name);
+    }
+  }
+
+  /**
+   * Add the supplied files/directories as children of this MockFile.
+   * This automatically sets isDirectory and resets isRegularFile on this
+   * MockFile, and registers this file as the parent of all the children.
+   */
+  MockFile addChildren(MockFile... children) {
+    Preconditions.checkState(isDirectory, "not a directory %s", getPath());
+    for (MockFile child : children) {
+      child.parent = this;
+      directoryContents.add(child);
+    }
+    return this;
+  }
+
+  /**
+   * Returns the child of the given name.
+   */
+  MockFile getChild(String name) throws FileNotFoundException {
+    Preconditions.checkNotNull(name, "name cannot be null");
+    Preconditions.checkState(isDirectory, "not a directory %s", getPath());
+    Iterator<MockFile> it = directoryContents.iterator();
+    while (it.hasNext()) {
+      MockFile f = it.next();
+      if (f.name.equals(name)) {
+        return f;
+      }
+    }
+    throw new FileNotFoundException(
+        "no such file: " + getPath() + SEPARATOR + name);
+  }
+
+  /**
+   * Return the path to this file or directory.
+   */
+  String getPath() {
+    if (parent == null) {
+      return name;
+    } else {
+      String parentPath = parent.getPath();
+      return (parentPath.endsWith(SEPARATOR))
+             ? parentPath + name : parentPath + SEPARATOR + name;
+    }
+  }
+
+  MockFile setName(String name) {
+    Preconditions.checkNotNull(name, "name cannot be null");
+    this.name = name;
+    return this;
+  }
+
+  /**
+   * Return the name to this file or directory.
+   */
+  String getName() {
+    return name;
+  }
+
+  /**
+   * Return the parent directory of this file or directory,
+   * or null if there is no parent.
+   */
+  MockFile getParent() {
+    return parent;
+  }
+
+  boolean isDirectory() throws IOException {
+    return isDirectory;
+  }
+
+  /** If false, maybe a directory, pipe, device, broken link, hidden, etc. */
+  MockFile setIsRegularFile(boolean isRegularFile) {
+    this.isRegularFile = isRegularFile;
+    return this;
+  }
+
+  boolean isRegularFile() throws IOException {
+    return isRegularFile;
+  }
+
+  MockFile setIsHidden(boolean isHidden) {
+    this.isHidden = isHidden;
+    return this;
+  }
+
+  boolean isHidden() throws IOException {
+    return isHidden;
+  }
+
+  MockFile setCreationTime(FileTime creationTime) {
+    Preconditions.checkNotNull(creationTime, "time cannot be null");
+    this.creationTime = creationTime;
+    return this;
+  }
+
+  FileTime getCreationTime() throws IOException {
+    return creationTime;
+  }
+
+  MockFile setLastModifiedTime(FileTime lastModifiedTime) {
+    Preconditions.checkNotNull(lastModifiedTime, "time cannot be null");
+    this.lastModifiedTime = lastModifiedTime;
+    return this;
+  }
+
+  FileTime getLastModifiedTime() throws IOException {
+    return lastModifiedTime;
+  }
+
+  /** Note that the adaptor calls this setter. */
+  MockFile setLastAccessTime(FileTime lastAccessTime) throws IOException {
+    Preconditions.checkNotNull(lastAccessTime, "time cannot be null");
+    this.lastAccessTime = lastAccessTime;
+    return this;
+  }
+
+  FileTime getLastAccessTime() throws IOException {
+    return lastAccessTime;
+  }
+
+  BasicFileAttributes readBasicAttributes() throws IOException {
+    return new MockBasicFileAttributes();
+  }
+
+  MockFile setDfsUncActiveStorageUnc(Path unc) {
+    this.dfsUncActiveStorageUnc = unc;
+    return this;
+  }
+
+  Path getDfsUncActiveStorageUnc() throws IOException {
+    return dfsUncActiveStorageUnc;
+  }
+
+  MockFile setDfsShareAclView(AclFileAttributeView aclView) {
+    this.dfsShareAclView = aclView;
+    return this;
+  }
+
+  AclFileAttributeView getDfsShareAclView() throws IOException {
+    return dfsShareAclView;
+  }
+
+  MockFile setShareAclView(AclFileAttributeView aclView) {
+    this.shareAclView = aclView;
+    return this;
+  }
+
+  AclFileAttributeView getShareAclView() throws IOException {
+    return (shareAclView == null) ? FULL_ACCESS_ACLVIEW : shareAclView;
+  }
+
+  MockFile setAclView(AclFileAttributeView aclView) {
+    this.aclView = aclView;
+    return this;
+  }
+
+  AclFileAttributeView getAclView() throws IOException {
+    if (aclView == null) {
+      return (parent == null) ? FULL_ACCESS_ACLVIEW : EMPTY_ACLVIEW;
+    } else {
+      return aclView;
+    }
+  }
+
+  MockFile setInheritedAclView(AclFileAttributeView aclView) {
+    this.inheritedAclView = aclView;
+    return this;
+  }
+
+  AclFileAttributeView getInheritedAclView() throws IOException {
+    if (inheritedAclView == null) {
+      if (parent == null) {
+        // root has no inherited ACL
+        return EMPTY_ACLVIEW;
+      } else if (parent.parent == null) {
+        // root's children inherit its ACL
+        return parent.getAclView();
+      } else {
+        // all other children inherit from their parent
+        return parent.getInheritedAclView();
+      }
+    } else {
+      return inheritedAclView;
+    }
+  }
+
+  MockFile setContentType(String contentType) {
+    this.contentType = contentType;
+    return this;
+  }
+
+  String getContentType() throws IOException {
+    return isRegularFile ? contentType : null;
+  }
+
+  MockFile setFileContents(String fileContents) {
+    Preconditions.checkNotNull(fileContents, "fileContents cannot be null");
+    setFileContents(fileContents.getBytes(Charsets.UTF_8));
+    if (contentType == null) {
+      contentType = "text/plain";
+    }
+    return this;
+  }
+
+  MockFile setFileContents(byte[] fileContents) {
+    Preconditions.checkState(isRegularFile, "not a regular file %s", getPath());
+    Preconditions.checkNotNull(fileContents, "fileContents cannot be null");
+    this.fileContents = fileContents;
+    return this;
+  }
+
+  InputStream newInputStream() throws IOException {
+    Preconditions.checkState(isRegularFile, "not a regular file %s", getPath());
+    return new ByteArrayInputStream(fileContents);
+  }
+
+  DirectoryStream<Path> newDirectoryStream() throws IOException {
+    if (!isDirectory) {
+      throw new NotDirectoryException("not a directory " + getPath());
+    }
+    return new MockDirectoryStream(directoryContents);
+  }
+
+  @Override
+  public String toString() {
+    return getPath();
+  }
+
+  private class MockBasicFileAttributes implements BasicFileAttributes {
+
+    @Override
+    public Object fileKey() {
+      return MockFile.this;
+    }
+
+    @Override
+    public FileTime creationTime() {
+      return creationTime;
+    }
+
+    @Override
+    public FileTime lastAccessTime() {
+      return lastAccessTime;
+    }
+
+    @Override
+    public FileTime lastModifiedTime() {
+      return lastModifiedTime;
+    }
+
+    @Override
+    public boolean isDirectory() {
+      return isDirectory;
+    }
+
+    @Override
+    public boolean isRegularFile() {
+      return isRegularFile;
+    }
+
+    @Override
+    public boolean isOther() {
+      return !(isDirectory || isRegularFile);
+    }
+
+    @Override
+    public boolean isSymbolicLink() {
+      return false;
+    }
+
+    @Override
+    public long size() {
+      if (isRegularFile && fileContents != null) {
+        return fileContents.length;
+      } else {
+        return 0L;
+      }
+    }
+  }
+
+  private class MockDirectoryStream implements DirectoryStream<Path> {
+    private Iterator<Path> iterator;
+
+    MockDirectoryStream(List<MockFile> files) {
+      ArrayList<Path> paths = new ArrayList<Path>();
+      for (MockFile file : files) {
+        paths.add(Paths.get(file.getPath()));
+      }
+      Collections.sort(paths);
+      iterator = paths.iterator();
+    }
+
+    @Override
+    public Iterator<Path> iterator() {
+      Preconditions.checkState(iterator != null,
+          "multiple attempts to get iterator");
+      Iterator<Path> rtn = iterator;
+      iterator = null;
+      return rtn;
+    }
+
+    @Override
+    public void close() {}
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
new file mode 100644
index 0000000..9a4c6f8
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFileDelegate.java
@@ -0,0 +1,178 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.common.base.Preconditions;
+import com.google.enterprise.adaptor.DocId;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.attribute.AclFileAttributeView;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.BlockingQueue;
+import java.util.Iterator;
+
+class MockFileDelegate implements FileDelegate {
+
+  private final MockFile root;
+
+  MockFileDelegate(MockFile root) {
+    Preconditions.checkNotNull(root, "root cannot be null");
+    this.root = root;
+  }
+
+  /**
+   * Returns the {@link MockFile} identified by the supplied {@link Path}.
+   * @throws FileNotFoundException if the file is not found.
+   */
+  MockFile getFile(Path doc) throws FileNotFoundException {
+    Preconditions.checkNotNull(doc, "doc cannot be null");
+    MockFile file = root;
+    Iterator<Path> iter = doc.iterator();
+    if (doc.getRoot() != null) {
+      // Using startsWith because Path adds a trailing backslash to
+      // UNC roots.  The second check accounts for Windows Path
+      // implementation flipping slashes on Unix paths.
+      if (!(doc.getRoot().toString().startsWith(root.getPath()) ||
+          root.getPath().equals(doc.getRoot().toString().replace('\\', '/')))) {
+        throw new FileNotFoundException("not found: " + doc.toString());
+      }
+    } else if (iter.hasNext()) {
+      if (!(root.getPath().equals(iter.next().toString()))) {
+        throw new FileNotFoundException("not found: " + doc.toString());
+      }
+    }
+    while (iter.hasNext()) {
+      file = file.getChild(iter.next().toString());
+    }
+    return file;
+  }
+
+  @Override
+  public Path getPath(String pathname) throws IOException {
+    Preconditions.checkNotNull(pathname, "pathname cannot be null");
+    return Paths.get(pathname);
+  }
+
+  @Override
+  public boolean isDirectory(Path doc) throws IOException {
+    try {
+      return getFile(doc).isDirectory();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public boolean isRegularFile(Path doc) throws IOException {
+    try {
+      return getFile(doc).isRegularFile();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public boolean isHidden(Path doc) throws IOException {
+    try {
+      return getFile(doc).isHidden();
+    } catch (FileNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public BasicFileAttributes readBasicAttributes(Path doc) throws IOException {
+    return getFile(doc).readBasicAttributes();
+  }
+
+  @Override
+  public void setLastAccessTime(Path doc, FileTime time) throws IOException {
+    getFile(doc).setLastAccessTime(time);
+  }
+
+  @Override
+  public String probeContentType(Path doc) throws IOException {
+    return getFile(doc).getContentType();
+  }
+
+  @Override
+  public InputStream newInputStream(Path doc) throws IOException {
+    return getFile(doc).newInputStream();
+  }
+
+  @Override
+  public DirectoryStream<Path> newDirectoryStream(Path doc) throws IOException {
+    return getFile(doc).newDirectoryStream();
+  }
+
+  @Override
+  public DocId newDocId(Path doc) throws IOException {
+    String id = doc.toString().replace('\\', '/');
+    if (isDirectory(doc) && !id.endsWith("/")) {
+      id += "/";
+    }
+    if (id.startsWith("//")) {
+      // String.replaceFirst uses regular expression string and replacement
+      // so they need to be escaped appropriately. The above String.replace
+      // does NOT use expressions so regex escaping is not needed.
+      id = id.replaceFirst("//", "\\\\\\\\");
+    }
+    return new DocId(id);
+  }
+
+  @Override
+  public AclFileAttributeViews getAclViews(Path doc) throws IOException {
+    MockFile file = getFile(doc);
+    return new AclFileAttributeViews(file.getAclView(),
+                                     file.getInheritedAclView());
+  }
+
+  @Override
+  public AclFileAttributeView getShareAclView(Path doc) throws IOException {
+    return root.getShareAclView();
+  }
+
+  @Override
+  public AclFileAttributeView getDfsShareAclView(Path doc) throws IOException {
+    return root.getDfsShareAclView();
+  }
+
+  @Override
+  public Path getDfsUncActiveStorageUnc(Path doc) throws IOException {
+    return root.getDfsUncActiveStorageUnc();
+  }
+
+  @Override
+  public void startMonitorPath(Path watchPath, BlockingQueue<Path> queue)
+    throws IOException {
+    // TODO (bmj): implementation
+  }
+
+  @Override
+  public void stopMonitorPath() {
+    // TODO (bmj): implementation
+  }
+
+  @Override
+  public void destroy() {
+    stopMonitorPath();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java b/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java
new file mode 100644
index 0000000..91f85f8
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFileDelegateTest.java
@@ -0,0 +1,222 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+import com.google.enterprise.adaptor.DocId;
+
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+
+/**
+ * Test cases for {@link MockFileDelegate}.
+ */
+public class MockFileDelegateTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstructorNullRoot() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFileDelegate(null);
+  }
+
+  @Test
+  public void testGetPathNullPathname() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFileDelegate(new MockFile("root", true)).getPath(null);
+  }
+
+  @Test
+  public void testGetPath() throws Exception {
+    String delim = File.separator;
+    FileDelegate delegate = new MockFileDelegate(new MockFile("root", true));
+    assertEquals("root", delegate.getPath("root").toString());
+    assertEquals("root" + delim + "foo" + delim + "bar",
+        delegate.getPath("root/foo/bar").toString());
+    assertEquals(delim + "foo" + delim + "bar" + delim + "baz",
+        delegate.getPath("/foo/bar/baz").toString());
+    assertEquals("\\\\server\\share\\dir\\file.txt",
+        delegate.getPath("\\\\server\\share\\dir\\file.txt").toString());
+  }
+
+  @Test
+  public void testGetFileNotFound() throws Exception {
+    MockFile root = new MockFile("root", true).addChildren(new MockFile("foo"));
+    MockFileDelegate delegate = new MockFileDelegate(root);
+    thrown.expect(FileNotFoundException.class);
+    delegate.getFile(delegate.getPath("root/nonExistent"));
+  }
+
+  @Test
+  public void testGetFileRelativeRoot() throws Exception {
+    testGetFile("root");
+  }
+
+  @Test
+  public void testGetFileUncRoot() throws Exception {
+    testGetFile("\\\\host\\share");
+    testGetFile("\\\\host\\share\\");
+  }
+
+  @Test
+  public void testGetFileUnixRoot() throws Exception {
+    testGetFile("/");
+  }
+
+  @Test
+  public void testGetFileDosRoot() throws Exception {
+    testGetFile("C:\\");
+  }
+
+  private void testGetFile(String rootPath) throws Exception {
+    MockFile root = new MockFile(rootPath, true).addChildren(
+        new MockFile("dir1", true).addChildren(new MockFile("foo")),
+        new MockFile("dir2", true).addChildren(new MockFile("bar")),
+        new MockFile("test.txt"));
+    MockFileDelegate delegate = new MockFileDelegate(root);
+
+    assertSame(root, delegate.getFile(delegate.getPath(rootPath)));
+    testGetFile(delegate, getPath(rootPath, "test.txt"));
+    testGetFile(delegate, getPath(rootPath, "dir1"));
+    testGetFile(delegate, getPath(rootPath, "dir2"));
+    testGetFile(delegate, getPath(rootPath, "dir1/foo"));
+    testGetFile(delegate, getPath(rootPath, "dir2/bar"));
+  }
+
+  private String getPath(String parent, String child) {
+    return (parent.endsWith("/")) ? parent + child : parent + "/" + child;
+  }
+
+  private void testGetFile(MockFileDelegate delegate, String pathname)
+      throws Exception {
+    Path path = delegate.getPath(pathname);
+    MockFile file = delegate.getFile(path);
+    assertEquals(pathname, file.getPath());
+    MockFile parent = file.getParent();
+    if (parent != null) {
+      assertSame(parent, delegate.getFile(path.getParent()));
+    }
+  }
+
+  @Test
+  public void testNewDocId() throws Exception {
+    MockFile root = new MockFile("root", true).addChildren(
+        new MockFile("dir1", true).addChildren(new MockFile("foo")));
+    FileDelegate delegate = new MockFileDelegate(root);
+    assertEquals(new DocId("root/"), getDocId(delegate, "root"));
+    assertEquals(new DocId("root/dir1/"), getDocId(delegate, "root/dir1"));
+    assertEquals(new DocId("root/dir1/foo"),
+                 getDocId(delegate, "root/dir1/foo"));
+  }
+
+  private DocId getDocId(FileDelegate delegate, String pathname)
+      throws Exception {
+    return delegate.newDocId(delegate.getPath(pathname));
+  }
+
+  @Test
+  public void testSetLastAccessTime() throws Exception {
+    MockFile root = new MockFile("root", true)
+        .addChildren(new MockFile("test.txt"));
+    FileDelegate delegate = new MockFileDelegate(root);
+    Path path = delegate.getPath("root/test.txt");
+
+    BasicFileAttributes attrs = delegate.readBasicAttributes(path);
+    assertEquals(MockFile.DEFAULT_FILETIME, attrs.lastAccessTime());
+
+    FileTime accessTime = FileTime.fromMillis(40000);
+    delegate.setLastAccessTime(path, accessTime);
+    attrs = delegate.readBasicAttributes(path);
+    assertEquals(accessTime, attrs.lastAccessTime());
+  }
+
+  /**
+   * Most of the MockDelegate methods are simple passthrough calls to the
+   * MockFile methods.  This is a sanity check to make sure I'm calling the
+   * right ones.
+   */
+  @Test
+  public void testPassthroughGetters() throws Exception {
+    FileTime createTime = FileTime.fromMillis(20000);
+    FileTime modifyTime = FileTime.fromMillis(30000);
+    FileTime accessTime = FileTime.fromMillis(40000);
+    String content = "<html><title>Hello World</title></html>";
+    MockFile root = new MockFile("root", true)
+        .addChildren(
+            new MockFile("test.html").setCreationTime(createTime)
+            .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime)
+            .setFileContents(content).setContentType("text/html"));
+
+    FileDelegate delegate = new MockFileDelegate(root);
+    Path path = delegate.getPath("root");
+    assertTrue(delegate.isDirectory(path));
+    assertFalse(delegate.isRegularFile(path));
+    assertNull(delegate.getDfsUncActiveStorageUnc(path));
+    Path uncPath = delegate.getPath("\\\\server\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    assertEquals(uncPath, delegate.getDfsUncActiveStorageUnc(path));
+    assertNull(delegate.getDfsShareAclView(path));
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW,
+                 delegate.getDfsShareAclView(path));
+    AclFileAttributeViews aclViews = delegate.getAclViews(path);
+    assertNotNull(aclViews);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, aclViews.getDirectAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, aclViews.getInheritedAclView());
+
+    path = delegate.getPath("root/test.html");
+    assertTrue(delegate.isRegularFile(path));
+    assertFalse(delegate.isDirectory(path));
+    assertFalse(delegate.isHidden(path));
+    assertEquals("text/html", delegate.probeContentType(path));
+    assertEquals(content, readContents(delegate, path));
+    aclViews = delegate.getAclViews(path);
+    assertNotNull(aclViews);
+    assertEquals(MockFile.EMPTY_ACLVIEW, aclViews.getDirectAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, aclViews.getInheritedAclView());
+
+    BasicFileAttributes attrs = delegate.readBasicAttributes(path);
+    assertTrue(attrs.isRegularFile());
+    assertFalse(attrs.isDirectory());
+    assertFalse(attrs.isOther());
+    assertEquals(createTime, attrs.creationTime());
+    assertEquals(modifyTime, attrs.lastModifiedTime());
+    assertEquals(accessTime, attrs.lastAccessTime());
+    assertEquals(content.getBytes(Charsets.UTF_8).length, attrs.size());
+  }
+
+  private String readContents(FileDelegate delegate, Path path)
+      throws IOException {
+    return CharStreams.toString(
+        new InputStreamReader(delegate.newInputStream(path), Charsets.UTF_8));
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockFileTest.java b/test/com/google/enterprise/adaptor/fs/MockFileTest.java
new file mode 100644
index 0000000..c65992b
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockFileTest.java
@@ -0,0 +1,309 @@
+// Copyright 2014 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.CharStreams;
+
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+
+/**
+ * Test cases for {@link MockFile}.
+ */
+public class MockFileTest {
+
+  private static final FileTime createTime = FileTime.fromMillis(20000);
+  private static final FileTime modifyTime = FileTime.fromMillis(30000);
+  private static final FileTime accessTime = FileTime.fromMillis(40000);
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testConstructorNullName() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFile(null);
+  }
+
+  /** Test constructor for regular files. */
+  @Test
+  public void testFileConstructor() throws Exception {
+    MockFile f = new MockFile("test");
+    checkDefaultConfig(f, "test", false);
+    assertEquals("Contents of test", readContents(f));
+    assertEquals("text/plain", f.getContentType());
+  }
+
+  /** Test constructor for directories . */
+  @Test
+  public void testDirectoryConstructor() throws Exception {
+    MockFile f = new MockFile("test", true);
+    checkDefaultConfig(f, "test", true);
+    DirectoryStream<Path> ds = f.newDirectoryStream();
+    assertNotNull(ds);
+    assertFalse(ds.iterator().hasNext());
+    ds.close();
+  }
+
+  /**
+   * Verifies some of the default file attributes (no explicit setters called).
+   */
+  private void checkDefaultConfig(MockFile file, String name, boolean isDir)
+      throws Exception {
+    assertEquals(name, file.getName());
+    assertEquals(isDir, file.isDirectory());
+    assertEquals(!isDir, file.isRegularFile());
+    assertFalse(file.isHidden());
+    assertNull(file.getParent());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getCreationTime());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getLastModifiedTime());
+    assertEquals(MockFile.DEFAULT_FILETIME, file.getLastAccessTime());
+  }
+
+  private String readContents(MockFile file) throws IOException {
+    return CharStreams.toString(
+        new InputStreamReader(file.newInputStream(), Charsets.UTF_8));
+  }
+
+  @Test
+  public void testSetName() throws Exception {
+    assertEquals("foo", new MockFile("test").setName("foo").getName());
+  }
+
+  @Test
+  public void testSetIsRegularFile() throws Exception {
+    MockFile f = new MockFile("test").setIsRegularFile(false);
+    assertFalse(f.isRegularFile());
+    assertFalse(f.isDirectory());
+
+    f.setIsRegularFile(true);
+    assertTrue(f.isRegularFile());
+    assertFalse(f.isDirectory());
+  }
+
+  @Test
+  public void testSetIsHidden() throws Exception {
+    MockFile f = new MockFile("test");
+    assertFalse(f.isHidden());  // default is false.
+    assertTrue(f.setIsHidden(true).isHidden());
+    assertFalse(f.setIsHidden(false).isHidden());
+  }
+
+  @Test
+  public void testSetFileTimes() throws Exception {
+    MockFile f = new MockFile("test").setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+    assertEquals(createTime, f.getCreationTime());
+    assertEquals(modifyTime, f.getLastModifiedTime());
+    assertEquals(accessTime, f.getLastAccessTime());
+  }
+
+  @Test
+  public void testReadBasicAttributesRegularFile() throws Exception {
+    MockFile f = new MockFile("test").setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertNotNull(bfa);
+    assertSame(f, bfa.fileKey());
+    assertEquals(createTime, bfa.creationTime());
+    assertEquals(modifyTime, bfa.lastModifiedTime());
+    assertEquals(accessTime, bfa.lastAccessTime());
+    assertTrue(bfa.isRegularFile());
+    assertFalse(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertFalse(bfa.isOther());
+    assertEquals(readContents(f).length(), bfa.size());
+  }
+
+  @Test
+  public void testReadBasicAttributesDirectory() throws Exception {
+    MockFile f = new MockFile("test", true).setCreationTime(createTime)
+        .setLastModifiedTime(modifyTime).setLastAccessTime(accessTime);
+
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertNotNull(bfa);
+    assertSame(f, bfa.fileKey());
+    assertEquals(createTime, bfa.creationTime());
+    assertEquals(modifyTime, bfa.lastModifiedTime());
+    assertEquals(accessTime, bfa.lastAccessTime());
+    assertFalse(bfa.isRegularFile());
+    assertTrue(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertFalse(bfa.isOther());
+    assertEquals(0L, bfa.size());
+  }
+
+  @Test
+  public void testReadBasicAttributesSpecialFile() throws Exception {
+    // If neither file, nor directory, then it is "special".
+    MockFile f = new MockFile("test", false).setIsRegularFile(false);
+    BasicFileAttributes bfa = f.readBasicAttributes();
+    assertFalse(bfa.isRegularFile());
+    assertFalse(bfa.isDirectory());
+    assertFalse(bfa.isSymbolicLink());
+    assertTrue(bfa.isOther());
+    assertEquals(0L, bfa.size());
+  }
+
+  @Test
+  public void testSetFileContents() throws Exception {
+    String expected = "Hello World";
+    MockFile f = new MockFile("test.txt").setFileContents(expected);
+    assertEquals(expected, readContents(f));
+  }
+
+  @Test
+  public void testSetFileContentsBytes() throws Exception {
+    String expected = "<html><title>Hello World</title></html>";
+    MockFile f = new MockFile("test.html")
+        .setFileContents(expected.getBytes(Charsets.UTF_8))
+        .setContentType("text/html");
+    assertEquals(expected, readContents(f));
+    assertEquals("text/html", f.getContentType());
+  }
+
+  @Test
+  public void testChildren() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile dir2 = new MockFile("dir2", true);
+    MockFile test = new MockFile("test.txt");
+    root.addChildren(test, dir1, dir2);
+    assertNull(root.getParent());
+    assertSame(root, dir1.getParent());
+    assertSame(root, dir2.getParent());
+    assertSame(root, test.getParent());
+    checkDirectoryListing(root, dir1, dir2, test);
+
+    // Test getChild().
+    assertSame(dir1, root.getChild("dir1"));
+    assertSame(dir2, root.getChild("dir2"));
+    assertSame(test, root.getChild("test.txt"));
+
+    // Add another file.
+    MockFile newer = new MockFile("newer.txt");
+    root.addChildren(newer);
+    checkDirectoryListing(root, dir1, dir2, newer, test);
+  }
+
+  private void checkDirectoryListing(MockFile parent, MockFile... children)
+      throws IOException {
+    DirectoryStream<Path> ds = parent.newDirectoryStream();
+    assertNotNull(ds);
+    Iterator<Path> iter = ds.iterator();
+    assertNotNull(iter);
+    for (MockFile child : children) {
+      assertEquals(child.getName(), iter.next().getFileName().toString());
+    }
+    assertFalse(iter.hasNext());
+    ds.close();
+  }
+
+  @Test
+  public void testGetPath() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile test = new MockFile("test.txt");
+    MockFile test1 = new MockFile("test.txt");
+    root.addChildren(test, dir1);
+    dir1.addChildren(test1);
+    assertEquals("root", root.getPath());
+    assertEquals("root/dir1", dir1.getPath());
+    assertEquals("root/test.txt", test.getPath());
+    assertEquals("root/dir1/test.txt", test1.getPath());
+  }
+
+  @Test
+  public void testGetChildNullName() throws Exception {
+    thrown.expect(NullPointerException.class);
+    new MockFile("root", true).getChild(null);
+  }
+
+  @Test
+  public void testGetChildNotFound() throws Exception {
+    thrown.expect(FileNotFoundException.class);
+    new MockFile("root", true).getChild("nonExistent");
+  }
+
+  @Test
+  public void testGetDfsUncActiveStorageUnc() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertNull(root.getDfsUncActiveStorageUnc());
+    Path uncPath = Paths.get("\\\\server\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    assertEquals(uncPath, root.getDfsUncActiveStorageUnc());
+  }
+
+  @Test
+  public void testGetDfsShareAclView() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertNull(root.getDfsShareAclView());
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getDfsShareAclView());
+  }
+
+  @Test
+  public void testDefaultRootAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getShareAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, root.getAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, root.getInheritedAclView());
+  }
+
+  @Test
+  public void testDefaultNonRootAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile dir1 = new MockFile("dir1", true);
+    MockFile foo = new MockFile("foo");
+    MockFile bar = new MockFile("bar");
+    root.addChildren(dir1, foo);
+    dir1.addChildren(bar);
+    assertEquals(MockFile.EMPTY_ACLVIEW, dir1.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, dir1.getInheritedAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getInheritedAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, bar.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, bar.getInheritedAclView());
+  }
+
+  @Test
+  public void testSetAclViews() throws Exception {
+    MockFile root = new MockFile("root", true);
+    MockFile foo = new MockFile("foo");
+    root.addChildren(foo);
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getInheritedAclView());
+
+    foo.setAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    foo.setInheritedAclView(MockFile.EMPTY_ACLVIEW);
+    assertEquals(MockFile.FULL_ACCESS_ACLVIEW, foo.getAclView());
+    assertEquals(MockFile.EMPTY_ACLVIEW, foo.getInheritedAclView());
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockRequest.java b/test/com/google/enterprise/adaptor/fs/MockRequest.java
new file mode 100644
index 0000000..850d2fa
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockRequest.java
@@ -0,0 +1,50 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.Request;
+
+import java.util.Date;
+
+/** A trivial implemenation of {@link Request} */
+class MockRequest implements Request {
+  private final DocId docid;
+  private final Date lastAccess;
+
+  MockRequest(DocId docid) {
+    this(docid, null);
+  }
+
+  MockRequest(DocId docid, Date lastAccess) {
+    this.docid = docid;
+    this.lastAccess = lastAccess;
+  }
+
+  @Override
+  public boolean hasChangedSinceLastAccess(Date lastModified) {
+    return lastModified.after(lastAccess);
+  }
+
+  @Override
+  public Date getLastAccessTime() {
+    return lastAccess;
+  }
+
+  @Override
+  public DocId getDocId() {
+    return docid;
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/MockResponse.java b/test/com/google/enterprise/adaptor/fs/MockResponse.java
new file mode 100644
index 0000000..edbbaaa
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/MockResponse.java
@@ -0,0 +1,124 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.Response;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * An implementation of {@link Response} that implements only those items that
+ * the adaptor uses.
+ */
+class MockResponse implements Response {
+
+  boolean notModified = false;
+  boolean notFound = false;
+  String contentType;
+  Date lastModified;
+  URI displayUrl;
+  Acl acl;
+  Map<String, String> metadata = new HashMap<String, String>();
+  Map<String, Acl> namedResources = new HashMap<String, Acl>();
+  ByteArrayOutputStream content;
+
+  @Override
+  public void respondNotModified() throws IOException {
+    notModified = true;
+  }
+
+  @Override
+  public void respondNotFound() throws IOException {
+    notFound = true;
+  }
+
+  @Override
+  public OutputStream getOutputStream() throws IOException {
+    content = new ByteArrayOutputStream();
+    return content;
+  }
+
+  @Override
+  public void setContentType(String contentType) {
+    this.contentType = contentType;
+  }
+
+  @Override
+  public void setLastModified(Date lastModified) {
+    this.lastModified = lastModified;
+  }
+
+  @Override
+  public void addMetadata(String key, String value) {
+    metadata.put(key, value);
+  }
+
+  @Override
+  public void setAcl(Acl acl) {
+    this.acl = acl;
+  }
+
+  @Override
+  public void putNamedResource(String fragment, Acl acl) {
+    namedResources.put(fragment, acl);
+  }
+
+  @Override
+  public void setDisplayUrl(URI displayUrl) {
+    this.displayUrl = displayUrl;
+  }
+
+  @Override
+  public void setSecure(boolean secure) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void addAnchor(URI uri, String text) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoIndex(boolean noIndex) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoFollow(boolean noFollow) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setNoArchive(boolean noArchive) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setCrawlOnce(boolean crawlOnce) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public void setLock(boolean lock) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java b/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
index ae5eaa7..4db3dce 100644
--- a/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
+++ b/test/com/google/enterprise/adaptor/fs/NioFileDelegateTest.java
@@ -45,15 +45,16 @@
   public TemporaryFolder temp = new TemporaryFolder();
 
   private Path newTempDir(String name) throws IOException {
-    return temp.newFolder(name).toPath();
+    return temp.newFolder(name).toPath().toRealPath();
   }
 
   private Path newTempFile(String name) throws IOException {
-    return temp.newFile(name).toPath();
+    return temp.newFile(name).toPath().toRealPath();
   }
 
   private Path newTempFile(Path parent, String name) throws IOException {
-    Preconditions.checkArgument(parent.startsWith(temp.getRoot().toPath()));
+    Preconditions.checkArgument(parent.startsWith(temp.getRoot().toPath()
+        .toRealPath()));
     return Files.createFile(parent.resolve(name));
   }
 
@@ -162,7 +163,7 @@
 
   @Test
   public void testNewDocId() throws Exception {
-    Path root = temp.getRoot().toPath();
+    Path root = temp.getRoot().toPath().toRealPath();
     Path dir = newTempDir("testDir");
     Path file = newTempFile(dir, "test");
 
diff --git a/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java b/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java
new file mode 100644
index 0000000..e9b3439
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/UnsupportedDocIdPusher.java
@@ -0,0 +1,83 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import com.google.enterprise.adaptor.Acl;
+import com.google.enterprise.adaptor.DocId;
+import com.google.enterprise.adaptor.DocIdPusher;
+import com.google.enterprise.adaptor.ExceptionHandler;
+import com.google.enterprise.adaptor.GroupPrincipal;
+import com.google.enterprise.adaptor.Principal;
+
+import java.util.Collection;
+import java.util.Map;
+
+/** Throws UnsupportedOperationException for all calls. */
+class UnsupportedDocIdPusher implements DocIdPusher {
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushDocIds(Iterable<DocId> docIds,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Record pushRecords(Iterable<Record> records,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public DocId pushNamedResources(Map<DocId, Acl> resources,
+      ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public GroupPrincipal pushGroupDefinitions(
+      Map<GroupPrincipal, ? extends Collection<Principal>> defs,
+      boolean caseSensitive) throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public GroupPrincipal pushGroupDefinitions(
+      Map<GroupPrincipal, ? extends Collection<Principal>> defs,
+      boolean caseSensitive, ExceptionHandler handler)
+      throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViewsTest.java b/test/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViewsTest.java
new file mode 100644
index 0000000..2b87cbe
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/fs/WindowsAclFileAttributeViewsTest.java
@@ -0,0 +1,340 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor.fs;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import com.sun.jna.platform.win32.Advapi32Util.Account;
+import com.sun.jna.platform.win32.Win32Exception;
+import com.sun.jna.platform.win32.WinError;
+import com.sun.jna.platform.win32.WinNT;
+import com.sun.jna.platform.win32.WinNT.SID_NAME_USE;
+
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryFlag;
+import java.nio.file.attribute.AclEntryPermission;
+import java.nio.file.attribute.AclEntryType;
+import java.nio.file.attribute.GroupPrincipal;
+import java.nio.file.attribute.UserPrincipal;
+import java.util.EnumSet;
+import java.util.Set;
+
+/** Tests for {@link WindowsAclFileAttributeViews} */
+public class WindowsAclFileAttributeViewsTest {
+
+  private final WindowsAclFileAttributeViews wafav =
+      new TestAclFileAttributeViews();
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testNewAclEntryUnsupportedAccessType() throws Exception {
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder()
+        .setType(WinNT.SYSTEM_AUDIT_ACE_TYPE).build();
+    assertNull(wafav.newAclEntry(ace));
+  }
+
+  @Test
+  public void testNewAclEntryUnresolvableSid() throws Exception {
+    TestHelper.assumeOsIsWindows(); // For new Win32Exception().
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder()
+        .setSid(new AccountSid(null)).build();
+    assertNull(wafav.newAclEntry(ace));
+  }
+
+  @Test
+  public void testNewAclEntryUnsupportedAccountType() throws Exception {
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder()
+        .setSid(new AccountSid(SID_NAME_USE.SidTypeUnknown, "", "")).build();
+    assertNull(wafav.newAclEntry(ace));
+  }
+
+  @Test
+  public void testNewAclEntryUserPrincipal() throws Exception {
+    testNewAclEntryUserPrincipal(AccountSid.user("userName", null), "userName");
+  }
+
+  @Test
+  public void testNewAclEntryUserWithDomainPrincipal() throws Exception {
+    testNewAclEntryUserPrincipal(AccountSid.user("userName", "domain"),
+                                 "domain\\userName");
+  }
+
+  private void testNewAclEntryUserPrincipal(AccountSid account,
+      String expectedName) throws Exception {
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder().setSid(account).build();
+    AclEntry aclEntry = wafav.newAclEntry(ace);
+    assertNotNull(aclEntry);
+    UserPrincipal principal = aclEntry.principal();
+    assertNotNull(principal);
+    assertFalse(principal instanceof GroupPrincipal);
+    assertEquals(expectedName, principal.getName());
+  }
+
+  @Test
+  public void testNewAclEntryGroupPrincipal() throws Exception {
+    testNewAclEntryGroupPrincipal(AccountSid.group("groupName", null),
+                                  "groupName");
+  }
+
+  @Test
+  public void testNewAclEntryGroupWithDomainPrincipal() throws Exception {
+    testNewAclEntryGroupPrincipal(AccountSid.group("groupName", "domain"),
+                                 "domain\\groupName");
+  }
+
+  @Test
+  public void testNewAclEntryAliasPrincipal() throws Exception {
+    AccountSid account =
+        new AccountSid(SID_NAME_USE.SidTypeAlias, "alias", "domain");
+    testNewAclEntryGroupPrincipal(account, "domain\\alias");
+  }
+
+  @Test
+  public void testNewAclEntryWellKnownGroupPrincipal() throws Exception {
+    AccountSid account =
+        new AccountSid(SID_NAME_USE.SidTypeWellKnownGroup, "wellKnown", null);
+    testNewAclEntryGroupPrincipal(account, "wellKnown");
+  }
+
+  private void testNewAclEntryGroupPrincipal(AccountSid account,
+      String expectedName) throws Exception {
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder().setSid(account).build();
+    AclEntry aclEntry = wafav.newAclEntry(ace);
+    assertNotNull(aclEntry);
+    UserPrincipal principal = aclEntry.principal();
+    assertNotNull(principal);
+    assertTrue(principal instanceof GroupPrincipal);
+    assertEquals(expectedName, principal.getName());
+  }
+
+  @Test
+  public void testNewAclEntryIndividualPermissions() throws Exception {
+    testNewAclEntryPermissions(WinNT.FILE_READ_DATA,
+                               AclEntryPermission.READ_DATA);
+    testNewAclEntryPermissions(WinNT.FILE_READ_ATTRIBUTES,
+                               AclEntryPermission.READ_ATTRIBUTES);
+    testNewAclEntryPermissions(WinNT.FILE_READ_EA,
+                               AclEntryPermission.READ_NAMED_ATTRS);
+    testNewAclEntryPermissions(WinNT.READ_CONTROL,
+                               AclEntryPermission.READ_ACL);
+    testNewAclEntryPermissions(WinNT.FILE_WRITE_DATA,
+                               AclEntryPermission.WRITE_DATA);
+    testNewAclEntryPermissions(WinNT.FILE_APPEND_DATA,
+                               AclEntryPermission.APPEND_DATA);
+    testNewAclEntryPermissions(WinNT.FILE_WRITE_ATTRIBUTES,
+                               AclEntryPermission.WRITE_ATTRIBUTES);
+    testNewAclEntryPermissions(WinNT.FILE_WRITE_EA,
+                               AclEntryPermission.WRITE_NAMED_ATTRS);
+    testNewAclEntryPermissions(WinNT.WRITE_DAC,
+                               AclEntryPermission.WRITE_ACL);
+    testNewAclEntryPermissions(WinNT.WRITE_OWNER,
+                               AclEntryPermission.WRITE_OWNER);
+    testNewAclEntryPermissions(WinNT.DELETE,
+                               AclEntryPermission.DELETE);
+    testNewAclEntryPermissions(WinNT.FILE_DELETE_CHILD,
+                               AclEntryPermission.DELETE_CHILD);
+    testNewAclEntryPermissions(WinNT.SYNCHRONIZE,
+                               AclEntryPermission.SYNCHRONIZE);
+    testNewAclEntryPermissions(WinNT.FILE_EXECUTE,
+                               AclEntryPermission.EXECUTE);
+  }
+
+  @Test
+  public void testNewAclEntryFullPermissions() throws Exception {
+    testNewAclEntryPermissions(WinNT.FILE_ALL_ACCESS,
+                               AclEntryPermission.values());
+
+  }
+
+  @Test
+  public void testNewAclEntryGenericPermissions() throws Exception {
+    testNewAclEntryPermissions(WinNT.GENERIC_READ, AclEntryPermission.READ_DATA,
+        AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_NAMED_ATTRS,
+        AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE);
+    testNewAclEntryPermissions(WinNT.GENERIC_WRITE,
+        AclEntryPermission.WRITE_DATA, AclEntryPermission.APPEND_DATA,
+        AclEntryPermission.READ_ACL, AclEntryPermission.WRITE_ATTRIBUTES,
+        AclEntryPermission.WRITE_NAMED_ATTRS, AclEntryPermission.SYNCHRONIZE);
+    testNewAclEntryPermissions(WinNT.GENERIC_EXECUTE,
+        AclEntryPermission.EXECUTE, AclEntryPermission.READ_ATTRIBUTES,
+        AclEntryPermission.READ_ACL, AclEntryPermission.SYNCHRONIZE);
+    testNewAclEntryPermissions(WinNT.GENERIC_ALL, AclEntryPermission.values());
+  }
+
+  private void testNewAclEntryPermissions(int acePermissions,
+      AclEntryPermission... expectedPermissions) throws Exception {
+    Set<AclEntryPermission> expected =
+        EnumSet.noneOf(AclEntryPermission.class);
+    for (AclEntryPermission perm : expectedPermissions) {
+      expected.add(perm);
+    }
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder()
+        .setSid(AccountSid.user("userName", null))
+        .setPerms(acePermissions)
+        .build();
+    AclEntry aclEntry = wafav.newAclEntry(ace);
+    assertNotNull(aclEntry);
+    assertEquals(expected, aclEntry.permissions());
+  }
+
+  @Test
+  public void testNewAclEntryIndividualFlags() throws Exception {
+    testNewAclEntryFlags(WinNT.OBJECT_INHERIT_ACE, AclEntryFlag.FILE_INHERIT);
+    testNewAclEntryFlags(WinNT.INHERIT_ONLY_ACE, AclEntryFlag.INHERIT_ONLY);
+    testNewAclEntryFlags(WinNT.CONTAINER_INHERIT_ACE,
+                         AclEntryFlag.DIRECTORY_INHERIT);
+    testNewAclEntryFlags(WinNT.NO_PROPAGATE_INHERIT_ACE,
+                         AclEntryFlag.NO_PROPAGATE_INHERIT);
+  }
+
+  @Test
+  public void testNewAclEntryMultipleFlags() throws Exception {
+    testNewAclEntryFlags((byte) (WinNT.OBJECT_INHERIT_ACE |
+        WinNT.CONTAINER_INHERIT_ACE | WinNT.INHERIT_ONLY_ACE |
+        WinNT.NO_PROPAGATE_INHERIT_ACE), AclEntryFlag.values());
+  }
+
+  private void testNewAclEntryFlags(byte aceFlags,
+      AclEntryFlag... expectedFlags) throws Exception {
+    Set<AclEntryFlag> expected = EnumSet.noneOf(AclEntryFlag.class);
+    for (AclEntryFlag flag : expectedFlags) {
+      expected.add(flag);
+    }
+    WinNT.ACCESS_ACEStructure ace = new AceBuilder()
+        .setSid(AccountSid.user("userName", null))
+        .setFlags(aceFlags)
+        .build();
+    AclEntry aclEntry = wafav.newAclEntry(ace);
+    assertNotNull(aclEntry);
+    assertEquals(expected, aclEntry.flags());
+  }
+
+  static class AceBuilder {
+    private Ace ace = new Ace();
+
+    public AceBuilder setType(byte type) {
+      ace.AceType = type;
+      return this;
+    }
+
+    public AceBuilder setFlags(byte... flags) {
+      for (byte flag : flags) {
+        ace.AceFlags |= flag;
+      }
+      return this;
+    }
+
+    public AceBuilder setPerms(int... perms) {
+      for (int perm : perms) {
+        ace.Mask |= perm;
+      }
+      return this;
+    }
+
+    public AceBuilder setSid(WinNT.PSID sid) {
+      ace.setSID(sid);
+      return this;
+    }
+
+    public WinNT.ACCESS_ACEStructure build() {
+      return ace;
+    }
+  }
+
+  static class Ace extends WinNT.ACCESS_ACEStructure {
+    // psid is not publicly settable in ACCESS_ACEStructure.
+    private WinNT.PSID sid;
+
+    public void setSID(WinNT.PSID sid) {
+      this.sid = sid;
+    }
+
+    @Override
+    public WinNT.PSID getSID() {
+      return (sid != null) ? sid : super.getSID();
+    }
+
+    @Override
+    public String getSidString() {
+      return (sid != null) ? sid.toString() : super.getSidString();
+    }
+  }
+
+  /** A SID implemention that wraps an Account, avoiding AD lookup. */
+  static class AccountSid extends WinNT.PSID {
+    private final Account account;
+
+    public static AccountSid user(String name, String domain) {
+      return new AccountSid(SID_NAME_USE.SidTypeUser, name, domain);
+    }
+
+    public static AccountSid group(String name, String domain) {
+      return new AccountSid(SID_NAME_USE.SidTypeGroup, name, domain);
+    }
+
+    public AccountSid(Account account) {
+      this.account = account;
+    }
+
+    public AccountSid(int type, String name, String domain) {
+      account = new Account();
+      account.accountType = type;
+      account.name = name;
+      account.domain = domain;
+    }
+
+    public Account getAccount() throws Win32Exception {
+      if (account == null) {
+        throw new Win32Exception(WinError.ERROR_NONE_MAPPED);
+      }
+      return account;
+    }
+
+    @Override
+    public String toString() {
+      if (account == null) {
+        return "null";
+      } else {
+        return (account.domain == null) ? account.name
+            : account.domain + "\\" + account.name;
+      }
+    }
+  }
+
+  /**
+   * An subclass of WindowsAclFileAttributeViews that avoids making
+   * actual Windows API calls.
+   */
+  static class TestAclFileAttributeViews extends WindowsAclFileAttributeViews {
+    public TestAclFileAttributeViews() {
+      super(null, null, null, null, null);
+    }
+
+    @Override
+    Account getAccountBySid(WinNT.PSID sid) throws Win32Exception {
+      if (sid instanceof AccountSid) {
+        return ((AccountSid) sid).getAccount();
+      } else {
+        return super.getAccountBySid(sid);
+      }
+    }
+  }
+}
diff --git a/test/com/google/enterprise/adaptor/fs/WindowsFileDelegateTest.java b/test/com/google/enterprise/adaptor/fs/WindowsFileDelegateTest.java
index f345598..0a536eb 100644
--- a/test/com/google/enterprise/adaptor/fs/WindowsFileDelegateTest.java
+++ b/test/com/google/enterprise/adaptor/fs/WindowsFileDelegateTest.java
@@ -72,11 +72,11 @@
   }
 
   private Path newTempDir(String name) throws IOException {
-    return temp.newFolder(name).toPath();
+    return temp.newFolder(name).toPath().toRealPath();
   }
 
   private Path newTempFile(String name) throws IOException {
-    return temp.newFile(name).toPath();
+    return temp.newFile(name).toPath().toRealPath();
   }
 
   private Path newTempFile(Path parent, String name) throws IOException {