Add MockForwarder

Allows NDN applications to test interest/data exchanges as if an NFD was present... but with no network IO
diff --git a/src/main/java/com/intel/jndn/mock/MockFace.java b/src/main/java/com/intel/jndn/mock/MockFace.java
index 8980fa7..4f73a02 100644
--- a/src/main/java/com/intel/jndn/mock/MockFace.java
+++ b/src/main/java/com/intel/jndn/mock/MockFace.java
@@ -77,7 +77,7 @@
   public final List<SignalOnSendData> onSendData = new ArrayList<>();
 
   private static final Logger LOGGER = Logger.getLogger(MockFace.class.getName());
-  private MockFaceTransport transport;
+  private MockTransport transport;
   private KeyChain keyChain;
 
   /////////////////////////////////////////////////////////////////////////////
@@ -175,7 +175,7 @@
    *   new MockFace(new Options());
    *   // use onSendInterest.add(handler) and onSendData.add(handler)
    *   // to add custom logic when Interest or Data packet are sent
-   *   // from the upper level (to transport)
+   *   // from the upper level (to callback)
    * </pre>
    *
    * To create Face that just logs packets in sentInterests and sentData:
@@ -186,8 +186,8 @@
    * @param options see {@link Options}
    */
   public MockFace(final Options options) {
-    super(new MockFaceTransport(), null);
-    transport = (MockFaceTransport) node_.getTransport();
+    super(new MockTransport(), null);
+    transport = (MockTransport) node_.getTransport();
     transport.setOnSendBlock(new OnIncomingPacket());
 
     try {
@@ -222,7 +222,7 @@
   /**
    * Route incoming packets to the correct callbacks.
    */
-  private class OnIncomingPacket implements MockFaceTransport.OnSendBlockSignal {
+  private class OnIncomingPacket implements MockTransport.OnSendBlockSignal {
     /**
      * {@inheritDoc}
      */
@@ -252,7 +252,7 @@
           LOGGER.info("Received an unknown packet");
         }
       } catch (EncodingException e) {
-        LOGGER.log(Level.INFO, "Failed to decode incoming packet", e);
+        LOGGER.log(Level.INFO, "Failed to decodeParameters incoming packet", e);
       }
     }
   }
@@ -309,7 +309,7 @@
   }
 
   /**
-   * Mock reception of the Interest packet on the Face (from transport).
+   * Mock reception of the Interest packet on the Face (from callback).
    *
    * @param interest the mock-remote interest to add to the PIT
    * @throws EncodingException if packet encoding fails (it should not)
@@ -319,7 +319,7 @@
   }
 
   /**
-   * Mock reception of the Data packet on the Face (from transport).
+   * Mock reception of the Data packet on the Face (from callback).
    *
    * @param data the mock-remote data to add to the CS
    * @throws EncodingException if packet encoding fails (it should not)
@@ -329,7 +329,7 @@
   }
 
   /**
-   * @return the transport for this face
+   * @return the callback for this face
    */
   public Transport getTransport() {
     return transport;
diff --git a/src/main/java/com/intel/jndn/mock/MockForwarder.java b/src/main/java/com/intel/jndn/mock/MockForwarder.java
new file mode 100644
index 0000000..dffc321
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/MockForwarder.java
@@ -0,0 +1,154 @@
+package com.intel.jndn.mock;
+
+import com.intel.jndn.mock.forwarder.BufferHandler;
+import com.intel.jndn.mock.forwarder.FibImpl;
+import com.intel.jndn.mock.forwarder.LocalFibEntry;
+import com.intel.jndn.mock.forwarder.OnPrefixRegistration;
+import com.intel.jndn.mock.forwarder.PitImpl;
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Face;
+import net.named_data.jndn.ForwardingFlags;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.security.KeyChain;
+import net.named_data.jndn.security.SecurityException;
+import net.named_data.jndn.transport.Transport;
+
+import java.util.Collection;
+import java.util.logging.Logger;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class MockForwarder {
+  private static final Logger LOGGER = Logger.getLogger(MockForwarder.class.getName());
+  private final KeyChain keyChain;
+  private final Name certName;
+  private final Fib fib = new FibImpl();
+  private final Pit pit = new PitImpl();
+
+  /**
+   * Forwarding information base API; use this for recording FIB entries
+   */
+  public interface Fib {
+    /**
+     * @param interest the incoming client interest
+     * @return all FIB entries matching the interest
+     */
+    Collection<FibEntry> find(Interest interest);
+
+    /**
+     * @param entry a new FIB entry to add; enables routing to the face (or more correctly, channel) contained in this
+     * entry
+     */
+    void add(FibEntry entry);
+  }
+
+  /**
+   * Entry in the FIB; use this for forwarding interest packets
+   */
+  public interface FibEntry {
+    /**
+     * @param interest the interest to forward
+     * @param sourceTransport the source of the interest for use in the callback
+     */
+    void forward(Interest interest, Transport sourceTransport);
+
+    /**
+     * @return then entry prefix name
+     */
+    Name getPrefix();
+
+    /**
+     * @return the entry flags
+     */
+    ForwardingFlags getFlags();
+  }
+
+  /**
+   * Pending interest table API; use this for recording incoming interests
+   */
+  public interface Pit {
+    /**
+     * @param entry the PIT entry to add
+     */
+    void add(PitEntry entry);
+
+    /**
+     * @param interest the incoming interest to match against
+     * @return true if the interest matches an entry already in the PIT
+     */
+    boolean has(Interest interest);
+
+    /**
+     * @param name the name to match against
+     * @return the PIT entries matching a name, removing them from the PIT
+     */
+    Collection<PitEntry> extract(Name name);
+  }
+
+  /**
+   * Entry in the PIT; use this for forwarding data packets
+   */
+  public interface PitEntry {
+    /**
+     * @param data the packet to forward
+     */
+    void forward(Data data);
+
+    /**
+     * @return the interest that first created the entry
+     */
+    Interest getInterest();
+
+    /**
+     * @return true if the entry has been satisfied (has had a matching data forwarded through it)
+     */
+    boolean isSatisfied();
+  }
+
+  /**
+   * Mock-specific API for recording the source and destination of incoming interests
+   */
+  public interface OnInterestReceived {
+    void in(Interest interest, Transport destinationTransport, Face sourceFace);
+  }
+
+  public MockForwarder() {
+    try {
+      keyChain = MockKeyChain.configure(new Name("/mock/forwarder"));
+      certName = keyChain.getDefaultCertificateName();
+    } catch (SecurityException e) {
+      throw new IllegalStateException("Failed to set up mock prefix registration", e);
+    }
+
+    OnPrefixRegistration onPrefixRegistration = new OnPrefixRegistration(keyChain, fib);
+    Name registrationPrefix = new Name("/localhost/nfd/rib/register");
+    register(registrationPrefix, onPrefixRegistration, new ForwardingFlags());
+  }
+
+  public Face connect() {
+    MockForwarderFace face = new MockForwarderFace();
+    face.setCommandSigningInfo(keyChain, certName);
+    LOGGER.info("Connected face with: " + face.getTransport());
+    return face;
+  }
+
+  public void register(Name prefix, OnInterestReceived callback, ForwardingFlags flags) {
+    Face registrationFace = this.connect();
+    FibEntry registrationEntry = new LocalFibEntry(prefix, callback, registrationFace, flags);
+    fib.add(registrationEntry);
+  }
+
+  private class MockForwarderFace extends Face {
+    MockForwarderFace() {
+      super(new MockTransport(), null);
+      MockTransport transport = (MockTransport) node_.getTransport();
+      transport.setOnSendBlock(new BufferHandler(transport, fib, pit));
+    }
+
+    Transport getTransport() {
+      return node_.getTransport();
+    }
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/MockTransport.java b/src/main/java/com/intel/jndn/mock/MockTransport.java
index df437e0..613aacf 100644
--- a/src/main/java/com/intel/jndn/mock/MockTransport.java
+++ b/src/main/java/com/intel/jndn/mock/MockTransport.java
@@ -31,9 +31,9 @@
  * @author Alexander Afanasyev, <aa@cs.ucla.edu>
  * @author Andrew Brown <andrew.brown@intel.com>
  */
-class MockFaceTransport extends Transport {
+public class MockTransport extends Transport {
   private OnSendBlockSignal onSendBlock;
-  private static final Logger LOGGER = Logger.getLogger(MockFaceTransport.class.getName());
+  private static final Logger LOGGER = Logger.getLogger(MockTransport.class.getName());
   private boolean connected;
   private ElementReader elementReader;
   private final List<ByteBuffer> receiveBuffer = new LinkedList<>();
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/BufferHandler.java b/src/main/java/com/intel/jndn/mock/forwarder/BufferHandler.java
new file mode 100644
index 0000000..4347075
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/BufferHandler.java
@@ -0,0 +1,87 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import com.intel.jndn.mock.MockTransport;
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.encoding.EncodingException;
+import net.named_data.jndn.encoding.TlvWireFormat;
+import net.named_data.jndn.encoding.tlv.Tlv;
+import net.named_data.jndn.encoding.tlv.TlvDecoder;
+import net.named_data.jndn.transport.Transport;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class BufferHandler implements MockTransport.OnSendBlockSignal {
+
+  private static final Logger LOGGER = Logger.getLogger(BufferHandler.class.getName());
+  private final MockTransport transport;
+  private final MockForwarder.Fib fib;
+  private final MockForwarder.Pit pit;
+
+  public BufferHandler(MockTransport transport, MockForwarder.Fib fib, MockForwarder.Pit pit) {
+    this.transport = transport;
+    this.fib = fib;
+    this.pit = pit;
+  }
+
+  @Override
+  public void emit(ByteBuffer buffer) {
+    try {
+      if (isInterest(buffer) || isData(buffer)) {
+        TlvDecoder decoder = new TlvDecoder(buffer);
+        if (decoder.peekType(Tlv.Interest, buffer.remaining())) {
+          Interest interest = new Interest();
+          interest.wireDecode(buffer, TlvWireFormat.get());
+          forward(interest, transport);
+        } else if (decoder.peekType(Tlv.Data, buffer.remaining())) {
+          Data data = new Data();
+          data.wireDecode(buffer, TlvWireFormat.get());
+          forward(data);
+        }
+      } else {
+        LOGGER.warning("Received an unknown packet");
+      }
+    } catch (EncodingException e) {
+      LOGGER.log(Level.INFO, "Failed to decodeParameters incoming packet", e);
+    }
+  }
+
+  private boolean isInterest(ByteBuffer buffer) {
+    return buffer.get(0) == Tlv.Interest;
+  }
+
+  private boolean isData(ByteBuffer buffer) {
+    return buffer.get(0) == Tlv.Data;
+  }
+
+  private void forward(Interest interest, Transport transport) {
+    if (pit.has(interest)) {
+      LOGGER.info("Already seen interest, swallowing: " + interest.toUri());
+      return;
+    }
+
+    LOGGER.info("Adding interest to PIT: " + interest.toUri());
+    pit.add(new PitEntryImpl(interest, (MockTransport) transport));
+
+    LOGGER.info("Forwarding interest: " + interest.toUri());
+    for (MockForwarder.FibEntry entry : fib.find(interest)) {
+      entry.forward(interest, transport);
+    }
+  }
+
+  private void forward(Data data) {
+    Collection<MockForwarder.PitEntry> found = pit.extract(data.getName());
+    LOGGER.log(Level.INFO, "Found {0} pending interests", found.size());
+
+    for (MockForwarder.PitEntry pendingInterest : found) {
+      pendingInterest.forward(data);
+    }
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/ClientFibEntry.java b/src/main/java/com/intel/jndn/mock/forwarder/ClientFibEntry.java
new file mode 100644
index 0000000..8322ded
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/ClientFibEntry.java
@@ -0,0 +1,43 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import com.intel.jndn.mock.MockTransport;
+import net.named_data.jndn.ForwardingFlags;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.transport.Transport;
+
+import java.util.logging.Logger;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+class ClientFibEntry implements MockForwarder.FibEntry {
+
+  private static final Logger LOGGER = Logger.getLogger(ClientFibEntry.class.getName());
+  private final Name prefix;
+  private final MockTransport transport;
+  private final ForwardingFlags flags;
+
+  ClientFibEntry(Name prefix, MockTransport transport, ForwardingFlags flags) {
+    this.prefix = prefix;
+    this.transport = transport;
+    this.flags = flags;
+  }
+
+  @Override
+  public void forward(Interest interest, Transport sourceTransport) {
+    LOGGER.info("Receiving interest on: " + this.transport);
+    transport.receive(interest.wireEncode().buf());
+  }
+
+  @Override
+  public Name getPrefix() {
+    return new Name(prefix);
+  }
+
+  @Override
+  public ForwardingFlags getFlags() {
+    return flags;
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/FibImpl.java b/src/main/java/com/intel/jndn/mock/forwarder/FibImpl.java
new file mode 100644
index 0000000..7c87b61
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/FibImpl.java
@@ -0,0 +1,37 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class FibImpl implements MockForwarder.Fib {
+
+  private final ConcurrentHashMap<Name, MockForwarder.FibEntry> fib = new ConcurrentHashMap<>();
+
+  @Override
+  public void add(MockForwarder.FibEntry entry) {
+    fib.put(entry.getPrefix(), entry);
+  }
+
+  public List<MockForwarder.FibEntry> find(Interest interest) {
+    ArrayList<MockForwarder.FibEntry> entries = new ArrayList<>();
+    for (int i = interest.getName().size(); i >= 0; i--) {
+      Name prefix = interest.getName().getPrefix(i);
+      MockForwarder.FibEntry entry = fib.get(prefix);
+      if (entry != null) {
+        entries.add(entry);
+        if (!entry.getFlags().getChildInherit() || entry.getFlags().getCapture()) {
+          break;
+        }
+      }
+    }
+    return entries;
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/LocalFibEntry.java b/src/main/java/com/intel/jndn/mock/forwarder/LocalFibEntry.java
new file mode 100644
index 0000000..5c0da0e
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/LocalFibEntry.java
@@ -0,0 +1,44 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import net.named_data.jndn.Face;
+import net.named_data.jndn.ForwardingFlags;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.transport.Transport;
+
+import java.util.logging.Logger;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class LocalFibEntry implements MockForwarder.FibEntry {
+
+  private static final Logger LOGGER = Logger.getLogger(LocalFibEntry.class.getName());
+  private final Name prefix;
+  private final MockForwarder.OnInterestReceived callback;
+  private final Face registrationFace;
+  private final ForwardingFlags flags;
+
+  public LocalFibEntry(Name prefix, MockForwarder.OnInterestReceived callback, Face registrationFace, ForwardingFlags flags) {
+    this.prefix = prefix;
+    this.callback = callback;
+    this.registrationFace = registrationFace;
+    this.flags = flags;
+  }
+
+  public void forward(Interest interest, Transport sourceTransport) {
+    LOGGER.info("Forwarding interest on: " + this.callback);
+    callback.in(interest, sourceTransport, registrationFace);
+  }
+
+  @Override
+  public Name getPrefix() {
+    return new Name(prefix);
+  }
+
+  @Override
+  public ForwardingFlags getFlags() {
+    return flags;
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/OnPrefixRegistration.java b/src/main/java/com/intel/jndn/mock/forwarder/OnPrefixRegistration.java
new file mode 100644
index 0000000..f213789
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/OnPrefixRegistration.java
@@ -0,0 +1,89 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import com.intel.jndn.mock.MockTransport;
+import net.named_data.jndn.ControlParameters;
+import net.named_data.jndn.ControlResponse;
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Face;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.encoding.EncodingException;
+import net.named_data.jndn.security.KeyChain;
+import net.named_data.jndn.security.SecurityException;
+import net.named_data.jndn.transport.Transport;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Handle prefix registration requests from clients to a mock forwarder; must conform to specification outlined in
+ * https://redmine.named-data.net/projects/nfd/wiki/RibMgmt.
+ *
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class OnPrefixRegistration implements MockForwarder.OnInterestReceived {
+  private static final Logger LOGGER = Logger.getLogger(OnPrefixRegistration.class.getName());
+  private static final int STATUS_CODE_OK = 200;
+  private static final int CONTROL_PARAMETERS_NAME_OFFSET = -5;
+  private static final int CONTROL_COMMAND_NAME_OFFSET = 3;
+  private final KeyChain keyChain;
+  private final MockForwarder.Fib fib;
+
+  public OnPrefixRegistration(KeyChain keyChain, MockForwarder.Fib fib) {
+    this.keyChain = keyChain;
+    this.fib = fib;
+  }
+
+  @Override
+  public void in(Interest interest, Transport destinationTransport, Face localFace) {
+    LOGGER.info("Received registration request: " + interest.toUri());
+    ControlParameters params = decodeParameters(interest);
+
+    MockForwarder.FibEntry entry = new ClientFibEntry(params.getName(), (MockTransport) destinationTransport, params.getForwardingFlags());
+    fib.add(entry);
+    LOGGER.info("Added new route " + params.getName() + " to: " + destinationTransport);
+
+    ControlResponse response = encodeResponse(params);
+
+    Data data = new Data();
+    data.setName(interest.getName());
+    data.setContent(response.wireEncode());
+    signResponse(data);
+
+    try {
+      localFace.putData(data);
+    } catch (IOException e) {
+      LOGGER.log(Level.SEVERE, "Failed to send registration response", e);
+    }
+  }
+
+  private ControlParameters decodeParameters(Interest interest) {
+    ControlParameters params = new ControlParameters();
+    try {
+      params.wireDecode(interest.getName().get(CONTROL_PARAMETERS_NAME_OFFSET).getValue());
+      params.setFaceId(1);
+      params.setOrigin(0);
+      params.setCost(0);
+    } catch (EncodingException e) {
+      throw new IllegalArgumentException("", e);
+    }
+    return params;
+  }
+
+  private ControlResponse encodeResponse(ControlParameters params) {
+    ControlResponse response = new ControlResponse();
+    response.setStatusCode(STATUS_CODE_OK);
+    response.setStatusText("OK");
+    response.setBodyAsControlParameters(params);
+    return response;
+  }
+
+  private void signResponse(Data data) {
+    try {
+      keyChain.sign(data);
+    } catch (SecurityException e) {
+      LOGGER.log(Level.FINE, "MockKeyChain signing failed", e);
+    }
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/PitEntryImpl.java b/src/main/java/com/intel/jndn/mock/forwarder/PitEntryImpl.java
new file mode 100644
index 0000000..74b9d7e
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/PitEntryImpl.java
@@ -0,0 +1,43 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import com.intel.jndn.mock.MockTransport;
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Interest;
+
+import java.util.logging.Logger;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+final class PitEntryImpl implements MockForwarder.PitEntry {
+
+  private static final Logger LOGGER = Logger.getLogger(PitEntryImpl.class.getName());
+  public final Interest interest;
+  private final MockTransport transport;
+  private boolean satisfied = false;
+
+  PitEntryImpl(Interest interest, MockTransport transport) {
+    this.interest = interest;
+    this.transport = transport;
+  }
+
+  public void forward(Data data) {
+    LOGGER.info("Forwarding data on: " + this.transport);
+
+    if (satisfied) {
+      LOGGER.warning("Data already forwarded for PIT entry: " + interest.toUri());
+    }
+
+    transport.receive(data.wireEncode().buf());
+    satisfied = true;
+  }
+
+  public Interest getInterest() {
+    return new Interest(interest);
+  }
+
+  public boolean isSatisfied() {
+    return satisfied;
+  }
+}
diff --git a/src/main/java/com/intel/jndn/mock/forwarder/PitImpl.java b/src/main/java/com/intel/jndn/mock/forwarder/PitImpl.java
new file mode 100644
index 0000000..3e7a476
--- /dev/null
+++ b/src/main/java/com/intel/jndn/mock/forwarder/PitImpl.java
@@ -0,0 +1,53 @@
+package com.intel.jndn.mock.forwarder;
+
+import com.intel.jndn.mock.MockForwarder;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.Name;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class PitImpl implements MockForwarder.Pit {
+
+  private final Map<Name, List<MockForwarder.PitEntry>> pit = new ConcurrentHashMap<>();
+
+  public List<MockForwarder.PitEntry> extract(Name name) {
+    ArrayList<MockForwarder.PitEntry> entries = new ArrayList<>();
+    for (int i = name.size(); i >= 0; i--) {
+      Name prefix = name.getPrefix(i);
+      List<MockForwarder.PitEntry> pendingInterests = pit.get(prefix);
+      if (pendingInterests != null) {
+        entries.addAll(pendingInterests);
+        pendingInterests.clear(); // TODO is this necessary
+      }
+    }
+    return entries;
+  }
+
+  public void add(MockForwarder.PitEntry entry) {
+    if (!pit.containsKey(entry.getInterest().getName())) {
+      pit.put(entry.getInterest().getName(), new ArrayList<MockForwarder.PitEntry>(1));
+    }
+    pit.get(entry.getInterest().getName()).add(entry);
+  }
+
+  public boolean has(Interest interest) {
+    List<MockForwarder.PitEntry> entries = pit.get(interest.getName());
+
+    // TODO simplify
+    if (entries != null && entries.size() > 0) {
+      return true;
+//        for(int i = 0; i < entries.size(); i++){
+//          if(entries.get(i).interest.equals(interest)){
+//            return true;
+//          }
+//        }
+    }
+    return false;
+  }
+}
diff --git a/src/test/java/com/intel/jndn/mock/MockFaceTest.java b/src/test/java/com/intel/jndn/mock/MockFaceTest.java
index 7c8bbc9..37b0bca 100644
--- a/src/test/java/com/intel/jndn/mock/MockFaceTest.java
+++ b/src/test/java/com/intel/jndn/mock/MockFaceTest.java
@@ -181,7 +181,7 @@
     }
     final State state = new State();
 
-    // connect transport
+    // connect callback
     face.registerPrefix(new Name("/fake/prefix"), (OnInterestCallback) null, new OnRegisterFailed() {
       @Override
       public void onRegisterFailed(final Name prefix) {
diff --git a/src/test/java/com/intel/jndn/mock/MockForwarderTest.java b/src/test/java/com/intel/jndn/mock/MockForwarderTest.java
new file mode 100644
index 0000000..3d5e0ba
--- /dev/null
+++ b/src/test/java/com/intel/jndn/mock/MockForwarderTest.java
@@ -0,0 +1,92 @@
+package com.intel.jndn.mock;
+
+import net.named_data.jndn.Data;
+import net.named_data.jndn.Face;
+import net.named_data.jndn.Interest;
+import net.named_data.jndn.InterestFilter;
+import net.named_data.jndn.Name;
+import net.named_data.jndn.OnData;
+import net.named_data.jndn.OnInterestCallback;
+import net.named_data.jndn.OnRegisterFailed;
+import net.named_data.jndn.OnRegisterSuccess;
+import net.named_data.jndn.OnTimeout;
+import net.named_data.jndn.security.KeyChain;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Andrew Brown, andrew.brown@intel.com
+ */
+public class MockForwarderTest {
+  private static final Logger LOGGER = Logger.getLogger(MockForwarderTest.class.getName());
+
+  @Test
+  public void usage() throws Exception {
+    Name prefix = new Name("/test");
+    MockForwarder forwarder = new MockForwarder();
+    Face a = forwarder.connect();
+    Face b = forwarder.connect();
+
+    LOGGER.info("Registering prefix: " + prefix);
+    final CountDownLatch response1 = new CountDownLatch(1);
+    a.registerPrefix(prefix, new OnInterestCallback() {
+      @Override
+      public void onInterest(Name prefix, Interest interest, Face face, long interestFilterId, InterestFilter filter) {
+        LOGGER.info("Received interest: " + interest.toUri());
+        try {
+          face.putData(new Data(interest.getName()));
+          LOGGER.info("Sent data to interest: " + interest.getName());
+        } catch (IOException e) {
+          LOGGER.info("Failed to send data for: " + interest.toUri());
+        }
+      }
+    }, new OnRegisterFailed() {
+      @Override
+      public void onRegisterFailed(Name prefix) {
+        LOGGER.severe("Failed to register prefix for: " + prefix);
+        response1.countDown();
+      }
+    }, new OnRegisterSuccess() {
+      @Override
+      public void onRegisterSuccess(Name prefix, long registeredPrefixId) {
+        LOGGER.info("Prefix registered: " + prefix);
+        response1.countDown();
+      }
+    });
+    a.processEvents();
+    response1.await(1, TimeUnit.SECONDS);
+    assertEquals(0, response1.getCount());
+
+    LOGGER.info("Sending interest to prefix: " + prefix);
+    final CountDownLatch response2 = new CountDownLatch(1);
+    final AtomicBoolean received = new AtomicBoolean(false);
+    b.expressInterest(prefix, new OnData() {
+      @Override
+      public void onData(Interest interest, Data data) {
+        LOGGER.info("Received data: " + data.getName());
+        response2.countDown();
+        received.set(true);
+      }
+    }, new OnTimeout() {
+      @Override
+      public void onTimeout(Interest interest) {
+        LOGGER.info("Failed to receive data for interest: " + interest.toUri());
+        response2.countDown();
+      }
+    });
+    b.processEvents();
+    a.processEvents();
+    b.processEvents();
+    a.processEvents();
+
+    response2.await(1, TimeUnit.SECONDS);
+    assertTrue(received.get());
+  }
+}
\ No newline at end of file