[tor-commits] [onionoo/master] Move test classes to subpackages.

karsten at torproject.org karsten at torproject.org
Thu Feb 19 12:11:06 UTC 2015


commit 0d426f38df0e77a6b8aed84a7295a061062cf9ca
Author: Karsten Loesing <karsten.loesing at gmx.net>
Date:   Thu Feb 19 13:09:24 2015 +0100

    Move test classes to subpackages.
---
 .../org/torproject/onionoo/DummyBridgeStatus.java  |   43 -
 .../org/torproject/onionoo/DummyConsensus.java     |  114 --
 .../torproject/onionoo/DummyDescriptorSource.java  |   90 --
 .../org/torproject/onionoo/DummyDocumentStore.java |  120 --
 .../org/torproject/onionoo/DummyStatusEntry.java   |   92 --
 .../java/org/torproject/onionoo/DummyTime.java     |   16 -
 .../org/torproject/onionoo/LookupServiceTest.java  |  596 ---------
 .../torproject/onionoo/ResourceServletTest.java    | 1383 -------------------
 .../onionoo/UptimeDocumentWriterTest.java          |  260 ----
 .../onionoo/UptimeStatusUpdaterTest.java           |  184 ---
 .../onionoo/docs/BandwidthStatusTest.java          |    2 +-
 .../onionoo/docs/DummyDocumentStore.java           |  120 ++
 .../onionoo/server/ResourceServletTest.java        | 1385 ++++++++++++++++++++
 .../onionoo/updater/DummyBridgeStatus.java         |   43 +
 .../torproject/onionoo/updater/DummyConsensus.java |  114 ++
 .../onionoo/updater/DummyDescriptorSource.java     |   90 ++
 .../onionoo/updater/DummyStatusEntry.java          |   92 ++
 .../onionoo/updater/LookupServiceTest.java         |  596 +++++++++
 .../onionoo/updater/UptimeStatusUpdaterTest.java   |  182 +++
 .../org/torproject/onionoo/util/DummyTime.java     |   16 +
 .../onionoo/writer/UptimeDocumentWriterTest.java   |  262 ++++
 21 files changed, 2901 insertions(+), 2899 deletions(-)

diff --git a/src/test/java/org/torproject/onionoo/DummyBridgeStatus.java b/src/test/java/org/torproject/onionoo/DummyBridgeStatus.java
deleted file mode 100644
index 35a9036..0000000
--- a/src/test/java/org/torproject/onionoo/DummyBridgeStatus.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import java.util.List;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import org.torproject.descriptor.BridgeNetworkStatus;
-import org.torproject.descriptor.NetworkStatusEntry;
-
-public class DummyBridgeStatus implements BridgeNetworkStatus {
-
-  public byte[] getRawDescriptorBytes() {
-    return null;
-  }
-
-  public List<String> getAnnotations() {
-    return null;
-  }
-
-  public List<String> getUnrecognizedLines() {
-    return null;
-  }
-
-  private long publishedMillis;
-  public void setPublishedMillis(long publishedMillis) {
-    this.publishedMillis = publishedMillis;
-  }
-  public long getPublishedMillis() {
-    return this.publishedMillis;
-  }
-
-  private SortedMap<String, NetworkStatusEntry> statusEntries =
-      new TreeMap<String, NetworkStatusEntry>();
-  public void addStatusEntry(NetworkStatusEntry statusEntry) {
-    this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
-  }
-  public SortedMap<String, NetworkStatusEntry> getStatusEntries() {
-    return this.statusEntries;
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/DummyConsensus.java b/src/test/java/org/torproject/onionoo/DummyConsensus.java
deleted file mode 100644
index 3fa0fdd..0000000
--- a/src/test/java/org/torproject/onionoo/DummyConsensus.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import java.util.List;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TreeMap;
-
-import org.torproject.descriptor.DirSourceEntry;
-import org.torproject.descriptor.DirectorySignature;
-import org.torproject.descriptor.NetworkStatusEntry;
-import org.torproject.descriptor.RelayNetworkStatusConsensus;
-
-public class DummyConsensus implements RelayNetworkStatusConsensus {
-
-  public byte[] getRawDescriptorBytes() {
-    return null;
-  }
-
-  public List<String> getAnnotations() {
-    return null;
-  }
-
-  public List<String> getUnrecognizedLines() {
-    return null;
-  }
-
-  public int getNetworkStatusVersion() {
-    return 0;
-  }
-
-  public String getConsensusFlavor() {
-    return null;
-  }
-
-  public int getConsensusMethod() {
-    return 0;
-  }
-
-  private long validAfterMillis;
-  public void setValidAfterMillis(long validAfterMillis) {
-    this.validAfterMillis = validAfterMillis;
-  }
-  public long getValidAfterMillis() {
-    return this.validAfterMillis;
-  }
-
-  public long getFreshUntilMillis() {
-    return 0;
-  }
-
-  public long getValidUntilMillis() {
-    return 0;
-  }
-
-  public long getVoteSeconds() {
-    return 0;
-  }
-
-  public long getDistSeconds() {
-    return 0;
-  }
-
-  public List<String> getRecommendedServerVersions() {
-    return null;
-  }
-
-  public List<String> getRecommendedClientVersions() {
-    return null;
-  }
-
-  public SortedSet<String> getKnownFlags() {
-    return null;
-  }
-
-  public SortedMap<String, Integer> getConsensusParams() {
-    return null;
-  }
-
-  public SortedMap<String, DirSourceEntry> getDirSourceEntries() {
-    return null;
-  }
-
-  private SortedMap<String, NetworkStatusEntry> statusEntries =
-      new TreeMap<String, NetworkStatusEntry>();
-  public void addStatusEntry(NetworkStatusEntry statusEntry) {
-    this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
-  }
-  public SortedMap<String, NetworkStatusEntry> getStatusEntries() {
-    return this.statusEntries;
-  }
-
-  public boolean containsStatusEntry(String fingerprint) {
-    return false;
-  }
-
-  public NetworkStatusEntry getStatusEntry(String fingerprint) {
-    return null;
-  }
-
-  public SortedMap<String, DirectorySignature> getDirectorySignatures() {
-    return null;
-  }
-
-  public SortedMap<String, Integer> getBandwidthWeights() {
-    return null;
-  }
-
-  public String getConsensusDigest() {
-    return null;
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/DummyDescriptorSource.java b/src/test/java/org/torproject/onionoo/DummyDescriptorSource.java
deleted file mode 100644
index 4dbf066..0000000
--- a/src/test/java/org/torproject/onionoo/DummyDescriptorSource.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package org.torproject.onionoo;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-import org.torproject.descriptor.Descriptor;
-import org.torproject.onionoo.updater.DescriptorListener;
-import org.torproject.onionoo.updater.DescriptorSource;
-import org.torproject.onionoo.updater.DescriptorType;
-
-public class DummyDescriptorSource extends DescriptorSource {
-
-  private Map<DescriptorType, Set<Descriptor>> descriptors =
-      new HashMap<DescriptorType, Set<Descriptor>>();
-
-  public void provideDescriptors(DescriptorType descriptorType,
-      Collection<Descriptor> descriptors) {
-    for (Descriptor descriptor : descriptors) {
-      this.addDescriptor(descriptorType, descriptor);
-    }
-  }
-
-  public void addDescriptor(DescriptorType descriptorType,
-      Descriptor descriptor) {
-    this.getDescriptorsByType(descriptorType).add(descriptor);
-  }
-
-  private Set<Descriptor> getDescriptorsByType(
-      DescriptorType descriptorType) {
-    if (!this.descriptors.containsKey(descriptorType)) {
-      this.descriptors.put(descriptorType, new HashSet<Descriptor>());
-    }
-    return this.descriptors.get(descriptorType);
-  }
-
-  private Map<DescriptorType, Set<DescriptorListener>>
-      descriptorListeners = new HashMap<DescriptorType,
-      Set<DescriptorListener>>();
-
-  public void registerDescriptorListener(DescriptorListener listener,
-      DescriptorType descriptorType) {
-    if (!this.descriptorListeners.containsKey(descriptorType)) {
-      this.descriptorListeners.put(descriptorType,
-          new HashSet<DescriptorListener>());
-    }
-    this.descriptorListeners.get(descriptorType).add(listener);
-  }
-
-  public void readDescriptors() {
-    Set<DescriptorType> descriptorTypes = new HashSet<DescriptorType>();
-    descriptorTypes.addAll(this.descriptorListeners.keySet());
-    for (DescriptorType descriptorType : descriptorTypes) {
-      boolean relay;
-      switch (descriptorType) {
-      case RELAY_CONSENSUSES:
-      case RELAY_SERVER_DESCRIPTORS:
-      case RELAY_EXTRA_INFOS:
-      case EXIT_LISTS:
-        relay = true;
-        break;
-      case BRIDGE_STATUSES:
-      case BRIDGE_SERVER_DESCRIPTORS:
-      case BRIDGE_EXTRA_INFOS:
-      case BRIDGE_POOL_ASSIGNMENTS:
-      default:
-        relay = false;
-        break;
-      }
-      if (this.descriptors.containsKey(descriptorType) &&
-          this.descriptorListeners.containsKey(descriptorType)) {
-        Set<DescriptorListener> listeners =
-            this.descriptorListeners.get(descriptorType);
-        for (Descriptor descriptor :
-            this.getDescriptorsByType(descriptorType)) {
-          for (DescriptorListener listener : listeners) {
-            listener.processDescriptor(descriptor, relay);
-          }
-        }
-      }
-    }
-  }
-
-  public void writeHistoryFiles() {
-    /* Nothing to do here. */
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/DummyDocumentStore.java b/src/test/java/org/torproject/onionoo/DummyDocumentStore.java
deleted file mode 100644
index f5601b0..0000000
--- a/src/test/java/org/torproject/onionoo/DummyDocumentStore.java
+++ /dev/null
@@ -1,120 +0,0 @@
-package org.torproject.onionoo;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-import org.torproject.onionoo.docs.Document;
-import org.torproject.onionoo.docs.DocumentStore;
-
-public class DummyDocumentStore extends DocumentStore {
-
-  private Map<Class<? extends Document>, SortedMap<String, Document>>
-      storedDocuments = new HashMap<Class<? extends Document>,
-      SortedMap<String, Document>>();
-
-  private static final String FINGERPRINT_NULL = "";
-
-  private <T extends Document> SortedMap<String, Document>
-      getStoredDocumentsByClass(Class<T> documentType) {
-    if (!this.storedDocuments.containsKey(documentType)) {
-      this.storedDocuments.put(documentType,
-          new TreeMap<String, Document>());
-    }
-    return this.storedDocuments.get(documentType);
-  }
-
-  public <T extends Document> void addDocument(T document,
-      String fingerprint) {
-    this.getStoredDocumentsByClass(document.getClass()).put(
-        fingerprint == null ? FINGERPRINT_NULL : fingerprint, document);
-  }
-
-  public <T extends Document> T getDocument(Class<T> documentType,
-      String fingerprint) {
-    return documentType.cast(this.getStoredDocumentsByClass(documentType).
-        get(fingerprint == null ? FINGERPRINT_NULL : fingerprint));
-  }
-
-  public void flushDocumentCache() {
-    /* Nothing to do. */
-  }
-
-  public String getStatsString() {
-    /* No statistics to return. */
-    return null;
-  }
-
-  private int performedListOperations = 0;
-  public int getPerformedListOperations() {
-    return this.performedListOperations;
-  }
-
-  public <T extends Document> SortedSet<String> list(
-      Class<T> documentType, long modifiedAfter) {
-    return this.list(documentType);
-  }
-
-  public <T extends Document> SortedSet<String> list(
-      Class<T> documentType) {
-    this.performedListOperations++;
-    SortedSet<String> fingerprints = new TreeSet<String>(
-        this.getStoredDocumentsByClass(documentType).keySet());
-    fingerprints.remove(FINGERPRINT_NULL);
-    return fingerprints;
-  }
-
-  private int performedRemoveOperations = 0;
-  public int getPerformedRemoveOperations() {
-    return this.performedRemoveOperations;
-  }
-
-  public <T extends Document> boolean remove(Class<T> documentType) {
-    return this.remove(documentType, null);
-  }
-
-  public <T extends Document> boolean remove(Class<T> documentType,
-      String fingerprint) {
-    this.performedRemoveOperations++;
-    return this.getStoredDocumentsByClass(documentType).remove(
-        fingerprint) != null;
-  }
-
-  private int performedRetrieveOperations = 0;
-  public int getPerformedRetrieveOperations() {
-    return this.performedRetrieveOperations;
-  }
-
-  public <T extends Document> T retrieve(Class<T> documentType,
-      boolean parse) {
-    return this.retrieve(documentType, parse, null);
-  }
-
-  public <T extends Document> T retrieve(Class<T> documentType,
-      boolean parse, String fingerprint) {
-    this.performedRetrieveOperations++;
-    return documentType.cast(this.getStoredDocumentsByClass(documentType).
-        get(fingerprint == null ? FINGERPRINT_NULL : fingerprint));
-  }
-
-  private int performedStoreOperations = 0;
-  public int getPerformedStoreOperations() {
-    return this.performedStoreOperations;
-  }
-
-  public <T extends Document> boolean store(T document) {
-    return this.store(document, null);
-  }
-
-  public <T extends Document> boolean store(T document,
-      String fingerprint) {
-    this.performedStoreOperations++;
-    this.getStoredDocumentsByClass(document.getClass()).put(
-        fingerprint == null ? FINGERPRINT_NULL : fingerprint, document);
-    return true;
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/DummyStatusEntry.java b/src/test/java/org/torproject/onionoo/DummyStatusEntry.java
deleted file mode 100644
index 8fdc5cd..0000000
--- a/src/test/java/org/torproject/onionoo/DummyStatusEntry.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import java.util.List;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import org.torproject.descriptor.NetworkStatusEntry;
-
-public class DummyStatusEntry implements NetworkStatusEntry {
-
-  public DummyStatusEntry(String fingerprint) {
-    this.fingerprint = fingerprint;
-  }
-
-  public byte[] getStatusEntryBytes() {
-    return null;
-  }
-
-  @Override
-  public String getNickname() {
-    return null;
-  }
-
-  private String fingerprint;
-  public String getFingerprint() {
-    return this.fingerprint;
-  }
-
-  public String getDescriptor() {
-    return null;
-  }
-
-  public long getPublishedMillis() {
-    return 0;
-  }
-
-  public String getAddress() {
-    return null;
-  }
-
-  public int getOrPort() {
-    return 0;
-  }
-
-  public int getDirPort() {
-    return 0;
-  }
-
-  public Set<String> getMicrodescriptorDigests() {
-    return null;
-  }
-
-  public List<String> getOrAddresses() {
-    return null;
-  }
-
-  private SortedSet<String> flags = new TreeSet<String>();
-  public void addFlag(String flag) {
-    this.flags.add(flag);
-  }
-  public SortedSet<String> getFlags() {
-    return this.flags;
-  }
-
-  public String getVersion() {
-    return null;
-  }
-
-  public long getBandwidth() {
-    return 0;
-  }
-
-  public long getMeasured() {
-    return 0;
-  }
-
-  public boolean getUnmeasured() {
-    return false;
-  }
-
-  public String getDefaultPolicy() {
-    return null;
-  }
-
-  public String getPortList() {
-    return null;
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/DummyTime.java b/src/test/java/org/torproject/onionoo/DummyTime.java
deleted file mode 100644
index ffbd6e3..0000000
--- a/src/test/java/org/torproject/onionoo/DummyTime.java
+++ /dev/null
@@ -1,16 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import org.torproject.onionoo.util.Time;
-
-public class DummyTime extends Time {
-  private long currentTimeMillis;
-  public DummyTime(long currentTimeMillis) {
-    this.currentTimeMillis = currentTimeMillis;
-  }
-  public long currentTimeMillis() {
-    return this.currentTimeMillis;
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/LookupServiceTest.java b/src/test/java/org/torproject/onionoo/LookupServiceTest.java
deleted file mode 100644
index 56c928d..0000000
--- a/src/test/java/org/torproject/onionoo/LookupServiceTest.java
+++ /dev/null
@@ -1,596 +0,0 @@
-/* Copyright 2013 The Tor Project
- * See LICENSE for licensing information */
-
-package org.torproject.onionoo;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
-import org.torproject.onionoo.updater.LookupResult;
-import org.torproject.onionoo.updater.LookupService;
-
-public class LookupServiceTest {
-
-  private List<String> geoLite2CityBlocksIPv4Lines,
-      geoLite2CityLocationsEnLines, geoipASNum2Lines;
-
-  private LookupService lookupService;
-
-  private SortedSet<String> addressStrings = new TreeSet<String>();
-
-  private SortedMap<String, LookupResult> lookupResults;
-
-  private void populateLines() {
-    this.geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    this.geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    this.geoLite2CityBlocksIPv4Lines.add("8.8.0.0/21,6252001,6252001,,0,"
-        + "0,,38.0000,-97.0000");
-    this.geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,5375480,6252001,,0,"
-        + "0,94035,37.3860,-122.0838");
-    this.geoLite2CityBlocksIPv4Lines.add("8.8.9.0/24,6252001,6252001,,0,"
-        + "0,,38.0000,-97.0000");
-    this.geoLite2CityLocationsEnLines = new ArrayList<String>();
-    this.geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    this.geoLite2CityLocationsEnLines.add("6252001,en,NA,"
-        + "\"North America\",US,\"United States\",,,,,,,");
-    this.geoLite2CityLocationsEnLines.add("5375480,en,NA,"
-        + "\"North America\",US,\"United States\",CA,California,,,"
-        + "\"Mountain View\",807,America/Los_Angeles");
-    this.geoipASNum2Lines = new ArrayList<String>();
-    this.geoipASNum2Lines.add("134743296,134744063,\"AS3356 Level 3 "
-        + "Communications\"");
-    this.geoipASNum2Lines.add("134744064,134744319,\"AS15169 Google "
-        + "Inc.\"");
-    this.geoipASNum2Lines.add("134744320,134750463,\"AS3356 Level 3 "
-        + "Communications\"");
-  }
-
-  private void writeCsvFiles() {
-    try {
-      this.writeCsvFile(this.geoLite2CityBlocksIPv4Lines,
-          "GeoLite2-City-Blocks-IPv4.csv");
-      this.writeCsvFile(this.geoLite2CityLocationsEnLines,
-          "GeoLite2-City-Locations-en.csv");
-      this.writeCsvFile(this.geoipASNum2Lines, "GeoIPASNum2.csv");
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  private void writeCsvFile(List<String> lines, String fileName)
-      throws IOException {
-    if (lines != null && !lines.isEmpty()) {
-      BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
-          new FileOutputStream(new File(this.tempGeoipDir, fileName)),
-          "UTF-8"));
-      for (String line : lines) {
-        bw.write(line + "\n");
-      }
-      bw.close();
-    }
-  }
-
-  private void performLookups() {
-    this.lookupService = new LookupService(this.tempGeoipDir);
-    this.lookupResults = this.lookupService.lookup(this.addressStrings);
-  }
-
-  private void assertLookupResult(List<String> geoLite2CityBlocksLines,
-      List<String> geoLite2CityLocationsLines,
-      List<String> geoipASNum2Lines, String addressString,
-      String countryCode, String countryName, String regionName,
-      String cityName, Float latitude, Float longitude, String aSNumber,
-      String aSName) {
-    this.addressStrings.add(addressString);
-    this.populateLines();
-    if (geoLite2CityBlocksLines != null) {
-      this.geoLite2CityBlocksIPv4Lines = geoLite2CityBlocksLines;
-    }
-    if (geoLite2CityLocationsLines != null) {
-      this.geoLite2CityLocationsEnLines = geoLite2CityLocationsLines;
-    }
-    if (geoipASNum2Lines != null) {
-      this.geoipASNum2Lines = geoipASNum2Lines;
-    }
-    this.writeCsvFiles();
-    /* Disable log messages printed to System.err. */
-    System.setErr(new PrintStream(new OutputStream() {
-      public void write(int b) {
-      }
-    }));
-    this.performLookups();
-    if (countryCode == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getCountryCode() == null);
-    } else {
-      assertEquals(countryCode,
-          this.lookupResults.get(addressString).getCountryCode());
-    }
-    if (countryName == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getCountryName() == null);
-    } else {
-      assertEquals(countryName,
-          this.lookupResults.get(addressString).getCountryName());
-    }
-    if (regionName == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getRegionName() == null);
-    } else {
-      assertEquals(regionName,
-          this.lookupResults.get(addressString).getRegionName());
-    }
-    if (cityName == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getCityName() == null);
-    } else {
-      assertEquals(cityName,
-          this.lookupResults.get(addressString).getCityName());
-    }
-    if (latitude == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getLatitude() == null);
-    } else {
-      assertEquals(latitude,
-          this.lookupResults.get(addressString).getLatitude(), 0.01);
-    }
-    if (longitude == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getLongitude() == null);
-    } else {
-      assertEquals(longitude,
-          this.lookupResults.get(addressString).getLongitude(), 0.01);
-    }
-    if (aSNumber == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getAsNumber() == null);
-    } else {
-      assertEquals(aSNumber,
-          this.lookupResults.get(addressString).getAsNumber());
-    }
-    if (aSName == null) {
-      assertTrue(!this.lookupResults.containsKey(addressString) ||
-          this.lookupResults.get(addressString).getAsName() == null);
-    } else {
-      assertEquals(aSName,
-          this.lookupResults.get(addressString).getAsName());
-    }
-  }
-
-  @Rule
-  public TemporaryFolder tempFolder = new TemporaryFolder();
-
-  private File tempGeoipDir;
-
-  @Before
-  public void createTempGeoipDir() throws IOException {
-    this.tempGeoipDir = this.tempFolder.newFolder("geoip");
-  }
-
-  @Test()
-  public void testLookup8888() {
-    this.assertLookupResult(null, null, null, "8.8.8.8", "us",
-        "United States", "California", "Mountain View", 37.3860f,
-        -122.0838f, "AS15169", "Google Inc.");
-  }
-
-  @Test()
-  public void testLookup8880() {
-    this.assertLookupResult(null, null, null, "8.8.8.0", "us",
-        "United States", "California", "Mountain View", 37.3860f,
-        -122.0838f, "AS15169", "Google Inc.");
-  }
-
-  @Test()
-  public void testLookup888255() {
-    this.assertLookupResult(null, null, null, "8.8.8.255", "us",
-        "United States", "California", "Mountain View", 37.3860f,
-        -122.0838f, "AS15169", "Google Inc.");
-  }
-
-  @Test()
-  public void testLookup888256() {
-    this.assertLookupResult(null, null, null, "8.8.8.256", null, null,
-        null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookup888Minus1() {
-    this.assertLookupResult(null, null, null, "8.8.8.-1", null, null,
-        null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookup000() {
-    this.assertLookupResult(null, null, null, "0.0.0.0", null, null, null,
-        null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupNoBlocksLines() {
-    this.assertLookupResult(new ArrayList<String>(), null, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupNoLocationLines() {
-    this.assertLookupResult(null, new ArrayList<String>(), null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupNoGeoipASNum2Lines() {
-    this.assertLookupResult(null, null, new ArrayList<String>(),
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupNoCorrespondingLocation() {
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("6252001,en,NA,"
-        + "\"North America\",US,\"United States\",,,,,,,");
-    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
-        "8.8.8.8", null, null, null, null, 37.3860f, -122.0838f,
-        "AS15169", "Google Inc.");
-  }
-
-  @Test()
-  public void testLookupBlocksStartNotANumber() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("one/24,5375480,6252001,,0,"
-        + "0,94035,37.3860,-122.0838");
-    this.assertLookupResult(
-        geoLite2CityBlocksIPv4Lines, null, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupBlocksLocationX() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,X,X,,0,0,94035,37.3860,"
-        + "-122.0838");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupBlocksLocationEmpty() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,,,,0,0,,,");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
-        "8.8.8.8", null, null, null, null, null, null, "AS15169",
-        "Google Inc.");
-  }
-
-  @Test()
-  public void testLookupBlocksTooFewFields() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,5375480,6252001,,0,"
-        + "0,94035,37.3860");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationLocIdNotANumber() {
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("threetwoonenineone,en,NA,"
-        + "\"North America\",US,\"United States\",CA,California,,,"
-        + "\"Mountain View\",807,America/Los_Angeles");
-    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationTooFewFields() {
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("threetwoonenineone,en,NA,"
-        + "\"North America\",US,\"United States\",CA,California,,,"
-        + "\"Mountain View\",807");
-    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
-        "8.8.8.8", null, null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupGeoipASNum2EndBeforeStart() {
-    List<String> geoipASNum2Lines = new ArrayList<String>();
-    geoipASNum2Lines.add("134743296,134744063,\"AS3356 Level 3 "
-        + "Communications\"");
-    geoipASNum2Lines.add("134744319,134744064,\"AS15169 Google Inc.\"");
-    geoipASNum2Lines.add("134744320,134750463,\"AS3356 Level 3 "
-        + "Communications\"");
-    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", "us",
-        "United States", "California", "Mountain View", 37.3860f,
-        -122.0838f, null, null);
-  }
-
-  @Test()
-  public void testLookupGeoipASNum2StartNotANumber() {
-    List<String> geoipASNum2Lines = new ArrayList<String>();
-    geoipASNum2Lines.add("one,134744319,\"AS15169 Google Inc.\"");
-    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
-        null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupGeoipASNum2StartTooLarge() {
-    List<String> geoipASNum2Lines = new ArrayList<String>();
-    geoipASNum2Lines.add("1" + String.valueOf(Long.MAX_VALUE)
-        + ",134744319,\"AS15169 Google Inc.\"");
-    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
-        null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupGeoipASNum2TooFewFields() {
-    List<String> geoipASNum2Lines = new ArrayList<String>();
-    geoipASNum2Lines.add("134744064,134744319");
-    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
-        null, null, null, null, null, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationTurkey() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("46.1.133.0/24,307515,298795,,0,0,,"
-        + "39.1458,34.1639");
-    geoLite2CityBlocksIPv4Lines.add("46.196.12.0/24,738927,298795,,0,0,,"
-        + "40.9780,27.5085");
-    geoLite2CityBlocksIPv4Lines.add("78.180.14.0/24,745169,298795,,0,0,,"
-        + "40.0781,29.5133");
-    geoLite2CityBlocksIPv4Lines.add("81.215.1.0/24,749748,298795,,0,0,,"
-        + "40.6000,33.6153");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("307515,en,AS,Asia,TR,Turkey,40,"
-        + "\"K\u0131r\u015Fehir\",,,\"K\u0131r\u015Fehir\",,"
-        + "Europe/Istanbul");
-    geoLite2CityLocationsEnLines.add("738927,en,AS,Asia,TR,Turkey,59,"
-        + "\"Tekirda\u011F\",,,\"Tekirda\u011F\",,Europe/Istanbul");
-    geoLite2CityLocationsEnLines.add("745169,en,AS,Asia,TR,Turkey,16,"
-        + "Bursa,,,\u0130neg\u00F6l,,Europe/Istanbul");
-    geoLite2CityLocationsEnLines.add("749748,en,AS,Asia,TR,Turkey,18,"
-        + "\"\u00C7ank\u0131r\u0131\",,,\"\u00C7ank\u0131r\u0131\",,"
-        + "Europe/Istanbul");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "46.1.133.0", "tr", "Turkey",
-        "K\u0131r\u015Fehir", "K\u0131r\u015Fehir", 39.1458f, 34.1639f,
-        null, null);
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "46.196.12.0", "tr", "Turkey",
-        "Tekirda\u011F", "Tekirda\u011F", 40.9780f, 27.5085f, null, null);
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "78.180.14.0", "tr", "Turkey",
-        "Bursa", "\u0130neg\u00F6l", 40.0781f, 29.5133f, null, null);
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "81.215.1.0", "tr", "Turkey",
-        "\u00C7ank\u0131r\u0131", "\u00C7ank\u0131r\u0131", 40.6000f,
-        33.6153f, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationLatvia() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("78.28.192.0/24,456202,458258,,0,0,,"
-        + "56.5000,27.3167");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("456202,en,EU,Europe,LV,Latvia,REZ,"
-        + "Rezekne,,,\"R\u0113zekne\",,Europe/Riga");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "78.28.192.0", "lv", "Latvia",
-        "Rezekne", "R\u0113zekne", 56.5000f, 27.3167f, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationAzerbaijan() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("94.20.148.0/24,585170,587116,,0,0,,"
-        + "41.1919,47.1706");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("585170,en,AS,Asia,AZ,Azerbaijan,"
-        + "SAK,\"Shaki City\",,,\"\u015E\u01DDki\",,Asia/Baku");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "94.20.148.0", "az",
-        "Azerbaijan", "Shaki City", "\u015E\u01DDki", 41.1919f, 47.1706f,
-        null, null);
-  }
-
-  @Test()
-  public void testLookupLocationVietnam() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("115.78.92.0/23,1587976,1562822,,0,0,"
-        + ",10.2333,106.3833");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("1587976,en,AS,Asia,VN,Vietnam,50,"
-        + "\"Tinh Ben Tre\",,,\"B\u1EBFn Tre\",,Asia/Ho_Chi_Minh");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "115.78.92.0", "vn",
-        "Vietnam", "Tinh Ben Tre", "B\u1EBFn Tre", 10.2333f, 106.3833f,
-        null, null);
-  }
-
-  @Test()
-  public void testLookupLocationJapan() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("113.154.131.0/24,1848333,1861060,,0,"
-        + "0,1012236,35.8000,139.1833");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("1848333,en,AS,Asia,JP,Japan,13,"
-        + "\"T\u014Dky\u014D\",,,Yokoo,,Asia/Tokyo");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "113.154.131.0", "jp",
-        "Japan", "T\u014Dky\u014D", "Yokoo", 35.8000f, 139.1833f, null,
-        null);
-  }
-
-  @Test()
-  public void testLookupLocationDenmark() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("2.110.246.0/24,2625001,2623032,,0,0,"
-        + "5970,54.8880,10.4112");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("2625001,en,EU,Europe,DK,Denmark,83,"
-        + "\"South Denmark\",,,\"\u00C6r\u00F8sk\u00F8bing\",,"
-        + "Europe/Copenhagen");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "2.110.246.0", "dk",
-        "Denmark", "South Denmark", "\u00C6r\u00F8sk\u00F8bing", 54.8880f,
-        10.4112f, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationGermany() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("37.209.30.128/25,2947444,2921044,,0,"
-        + "0,,48.6833,9.0167");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("2947444,en,EU,Europe,DE,Germany,BW,"
-        + "\"Baden-W\u00FCrttemberg Region\",,,B\u00F6blingen,,"
-        + "Europe/Berlin");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "37.209.30.128", "de",
-        "Germany", "Baden-W\u00FCrttemberg Region", "B\u00F6blingen",
-        48.6833f, 9.0167f, null, null);
-  }
-
-  @Test()
-  public void testLookupLocationPoland() {
-    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
-    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
-        + "registered_country_geoname_id,represented_country_geoname_id,"
-        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
-        + "longitude");
-    geoLite2CityBlocksIPv4Lines.add("5.185.94.0/24,3099434,798544,,0,0,,"
-        + "54.3608,18.6583");
-    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
-    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
-        + "continent_code,continent_name,country_iso_code,country_name,"
-        + "subdivision_1_iso_code,subdivision_1_name,"
-        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
-        + "metro_code,time_zone");
-    geoLite2CityLocationsEnLines.add("3099434,en,EU,Europe,PL,Poland,PM,"
-        + "\"Pomeranian Voivodeship\",,,\"Gda\u0144sk\",,Europe/Warsaw");
-    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
-        geoLite2CityLocationsEnLines, null, "5.185.94.0", "pl", "Poland",
-        "Pomeranian Voivodeship", "Gda\u0144sk", 54.3608f, 18.6583f, null,
-        null);
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/ResourceServletTest.java b/src/test/java/org/torproject/onionoo/ResourceServletTest.java
deleted file mode 100644
index cc48295..0000000
--- a/src/test/java/org/torproject/onionoo/ResourceServletTest.java
+++ /dev/null
@@ -1,1383 +0,0 @@
-/* Copyright 2013 The Tor Project
- * See LICENSE for licensing information */
-
-package org.torproject.onionoo;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.torproject.onionoo.docs.DateTimeHelper;
-import org.torproject.onionoo.docs.DocumentStoreFactory;
-import org.torproject.onionoo.docs.UpdateStatus;
-import org.torproject.onionoo.server.HttpServletRequestWrapper;
-import org.torproject.onionoo.server.HttpServletResponseWrapper;
-import org.torproject.onionoo.server.NodeIndexer;
-import org.torproject.onionoo.server.NodeIndexerFactory;
-import org.torproject.onionoo.server.ResourceServlet;
-import org.torproject.onionoo.util.Time;
-import org.torproject.onionoo.util.TimeFactory;
-
-import com.google.gson.Gson;
-
-/* TODO This test class could (should?) be split into ResponseBuilderTest
- * which tests ResponseBuilder and a much shorter ResourceServletTest
- * which tests servlet specifics. */
-public class ResourceServletTest {
-
-  private SortedMap<String, org.torproject.onionoo.docs.SummaryDocument>
-      relays, bridges;
-
-  private long currentTimeMillis = DateTimeHelper.parse(
-      "2013-04-24 12:22:22");
-
-  private class TestingHttpServletRequestWrapper
-      extends HttpServletRequestWrapper {
-    private String requestURI;
-    private String queryString;
-    private Map<String, String[]> parameterMap;
-    private TestingHttpServletRequestWrapper(String requestURI,
-        String queryString, Map<String, String[]> parameterMap) {
-      super(null);
-      this.requestURI = requestURI;
-      this.queryString = queryString;
-      this.parameterMap = parameterMap == null
-          ? new HashMap<String, String[]>() : parameterMap;
-    }
-    protected String getRequestURI() {
-      return this.requestURI;
-    }
-    @SuppressWarnings("rawtypes")
-    protected Map getParameterMap() {
-      return this.parameterMap;
-    }
-    protected String[] getParameterValues(String parameterKey) {
-      return this.parameterMap.get(parameterKey);
-    }
-    protected String getQueryString() {
-      return this.queryString;
-    }
-  }
-
-  private class TestingHttpServletResponseWrapper extends
-      HttpServletResponseWrapper {
-    private TestingHttpServletResponseWrapper() {
-      super(null);
-    }
-    private int errorStatusCode;
-    protected void sendError(int errorStatusCode) throws IOException {
-      this.errorStatusCode = errorStatusCode;
-    }
-    private Map<String, String> headers = new HashMap<String, String>();
-    protected void setHeader(String headerName, String headerValue) {
-      this.headers.put(headerName, headerValue);
-    }
-    protected void setContentType(String contentType) {
-    }
-    protected void setCharacterEncoding(String characterEncoding) {
-    }
-    private StringWriter stringWriter;
-    protected PrintWriter getWriter() throws IOException {
-      if (this.stringWriter == null) {
-        this.stringWriter = new StringWriter();
-        return new PrintWriter(this.stringWriter);
-      } else {
-        throw new IOException("Can only request writer once");
-      }
-    }
-    private String getWrittenContent() {
-      return this.stringWriter == null ? null
-          : this.stringWriter.toString();
-    }
-  }
-
-  private TestingHttpServletRequestWrapper request;
-
-  private TestingHttpServletResponseWrapper response;
-
-  private String responseString;
-
-  private SummaryDocument summaryDocument;
-
-  @Before
-  public void createSampleRelaysAndBridges() {
-    org.torproject.onionoo.docs.SummaryDocument relayTorkaZ =
-        new org.torproject.onionoo.docs.SummaryDocument(true, "TorkaZ",
-        "000C5F55BD4814B917CC474BD537F1A3B33CCE2A", Arrays.asList(
-        new String[] { "62.216.201.221", "62.216.201.222",
-        "62.216.201.223" }), DateTimeHelper.parse("2013-04-19 05:00:00"),
-        false, new TreeSet<String>(Arrays.asList(new String[] { "Running",
-        "Valid" })), 20L, "de",
-        DateTimeHelper.parse("2013-04-18 05:00:00"), "AS8767",
-        "torkaz <klaus dot zufall at gmx dot de> "
-        + "<fb-token:np5_g_83jmf=>", new TreeSet<String>(Arrays.asList(
-        new String[] { "001C13B3A55A71B977CA65EC85539D79C653A3FC",
-        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B" })));
-    org.torproject.onionoo.docs.SummaryDocument relayFerrari458 =
-        new org.torproject.onionoo.docs.SummaryDocument(true, "Ferrari458",
-        "001C13B3A55A71B977CA65EC85539D79C653A3FC", Arrays.asList(
-        new String[] { "68.38.171.200", "[2001:4f8:3:2e::51]" }),
-        DateTimeHelper.parse("2013-04-24 12:00:00"), true,
-        new TreeSet<String>(Arrays.asList(new String[] { "Fast", "Named",
-        "Running", "V2Dir", "Valid" })), 1140L, "us",
-        DateTimeHelper.parse("2013-02-12 16:00:00"), "AS7922", null,
-        new TreeSet<String>(Arrays.asList(new String[] {
-        "000C5F55BD4814B917CC474BD537F1A3B33CCE2A" })));
-    org.torproject.onionoo.docs.SummaryDocument relayTimMayTribute =
-        new org.torproject.onionoo.docs.SummaryDocument(true, "TimMayTribute",
-        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", Arrays.asList(
-        new String[] { "89.69.68.246" }),
-        DateTimeHelper.parse("2013-04-22 20:00:00"), false,
-        new TreeSet<String>(Arrays.asList(new String[] { "Fast",
-            "Running", "Unnamed", "V2Dir", "Valid" })), 63L, "a1",
-        DateTimeHelper.parse("2013-04-16 18:00:00"), "AS6830",
-        "1024D/51E2A1C7 steven j. murdoch "
-        + "<tor+steven.murdoch at cl.cam.ac.uk> <fb-token:5sr_k_zs2wm=>",
-        new TreeSet<String>());
-    org.torproject.onionoo.docs.SummaryDocument bridgeec2bridgercc7f31fe =
-        new org.torproject.onionoo.docs.SummaryDocument(false,
-        "ec2bridgercc7f31fe", "0000831B236DFF73D409AD17B40E2A728A53994F",
-        Arrays.asList(new String[] { "10.199.7.176" }),
-        DateTimeHelper.parse("2013-04-21 18:07:03"), false,
-        new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
-        null, DateTimeHelper.parse("2013-04-20 15:37:04"), null, null,
-        null);
-    org.torproject.onionoo.docs.SummaryDocument bridgeUnnamed =
-        new org.torproject.onionoo.docs.SummaryDocument(false, "Unnamed",
-        "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C", Arrays.asList(
-        new String[] { "10.0.52.84" }),
-        DateTimeHelper.parse("2013-04-20 17:37:04"), false,
-        new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
-        null, DateTimeHelper.parse("2013-04-14 07:07:05"), null, null,
-        null);
-    org.torproject.onionoo.docs.SummaryDocument bridgegummy =
-        new org.torproject.onionoo.docs.SummaryDocument(false, "gummy",
-        "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", Arrays.asList(
-        new String[] { "10.63.169.98" }),
-        DateTimeHelper.parse("2013-04-24 01:07:04"), true,
-        new TreeSet<String>(Arrays.asList(new String[] { "Running",
-        "Valid" })), -1L, null,
-        DateTimeHelper.parse("2013-01-16 21:07:04"), null, null, null);
-    this.relays =
-        new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>();
-    this.relays.put("000C5F55BD4814B917CC474BD537F1A3B33CCE2A",
-        relayTorkaZ);
-    this.relays.put("001C13B3A55A71B977CA65EC85539D79C653A3FC",
-        relayFerrari458);
-    this.relays.put("0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B",
-        relayTimMayTribute);
-    this.bridges =
-        new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>();
-    this.bridges.put("0000831B236DFF73D409AD17B40E2A728A53994F",
-        bridgeec2bridgercc7f31fe);
-    this.bridges.put("0002D9BDBBC230BD9C78FF502A16E0033EF87E0C",
-        bridgeUnnamed);
-    this.bridges.put("1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756",
-        bridgegummy);
-  }
-
-  private void runTest(String request) {
-    try {
-      this.createDummyTime();
-      this.createDummyDocumentStore();
-      this.createNodeIndexer();
-      this.makeRequest(request);
-      this.parseResponse();
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  private void createDummyTime() {
-    Time dummyTime = new DummyTime(this.currentTimeMillis);
-    TimeFactory.setTime(dummyTime);
-  }
-
-  private void createDummyDocumentStore() {
-    DummyDocumentStore documentStore = new DummyDocumentStore();
-    UpdateStatus updateStatus = new UpdateStatus();
-    updateStatus.setUpdatedMillis(this.currentTimeMillis);
-    documentStore.addDocument(updateStatus, null);
-    for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e :
-        this.relays.entrySet()) {
-      documentStore.addDocument(e.getValue(), e.getKey());
-    }
-    for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e :
-        this.bridges.entrySet()) {
-      documentStore.addDocument(e.getValue(), e.getKey());
-    }
-    DocumentStoreFactory.setDocumentStore(documentStore);
-  }
-
-  private void createNodeIndexer() {
-    NodeIndexer newNodeIndexer = new NodeIndexer();
-    newNodeIndexer.startIndexing();
-    NodeIndexerFactory.setNodeIndexer(newNodeIndexer);
-  }
-
-  private void makeRequest(String request) throws IOException {
-    ResourceServlet rs = new ResourceServlet();
-    String requestParts[] = request.split("\\?");
-    String path = requestParts[0];
-    String queryString = requestParts.length > 1 ? requestParts[1] : null;
-    Map<String, String[]> parameterMap = parseParameters(request);
-    this.request = new TestingHttpServletRequestWrapper(path, queryString,
-        parameterMap);
-    this.response = new TestingHttpServletResponseWrapper();
-    rs.doGet(this.request, this.response);
-  }
-
-  private void parseResponse() {
-    this.responseString = this.response.getWrittenContent();
-    if (this.responseString != null) {
-      Gson gson = new Gson();
-      this.summaryDocument = gson.fromJson(this.responseString,
-          SummaryDocument.class);
-    }
-  }
-
-  private void assertErrorStatusCode(String request,
-      int errorStatusCode) {
-    this.runTest(request);
-    assertEquals(errorStatusCode, this.response.errorStatusCode);
-  }
-
-  private void assertSummaryDocument(String request,
-      int expectedRelaysNumber, String[] expectedRelaysNicknames,
-      int expectedBridgesNumber, String[] expectedBridgesNicknames) {
-    this.runTest(request);
-    assertNotNull(this.summaryDocument);
-    assertEquals(expectedRelaysNumber,
-        this.summaryDocument.relays.length);
-    if (expectedRelaysNicknames != null) {
-      for (int i = 0; i < expectedRelaysNumber; i++) {
-        assertEquals(expectedRelaysNicknames[i],
-            this.summaryDocument.relays[i].n);
-      }
-    }
-    assertEquals(expectedBridgesNumber,
-        this.summaryDocument.bridges.length);
-    if (expectedBridgesNicknames != null) {
-      for (int i = 0; i < expectedBridgesNumber; i++) {
-        assertEquals(expectedBridgesNicknames[i],
-            this.summaryDocument.bridges[i].n);
-      }
-    }
-  }
-
-  private Map<String, String[]> parseParameters(String request) {
-    Map<String, String[]> parameters = null;
-    String[] uriParts = request.split("\\?");
-    if (uriParts.length == 2) {
-      Map<String, List<String>> parameterLists =
-          new HashMap<String, List<String>>();
-      for (String parameter : uriParts[1].split("&")) {
-        String[] parameterParts = parameter.split("=");
-        if (!parameterLists.containsKey(parameterParts[0])) {
-          parameterLists.put(parameterParts[0],
-              new ArrayList<String>());
-        }
-        parameterLists.get(parameterParts[0]).add(parameterParts[1]);
-      }
-      parameters = new HashMap<String, String[]>();
-      for (Map.Entry<String, List<String>> e :
-          parameterLists.entrySet()) {
-        parameters.put(e.getKey(),
-            e.getValue().toArray(new String[e.getValue().size()]));
-      }
-    }
-    return parameters;
-  }
-
-  private static class SummaryDocument {
-    private String relays_published;
-    private RelaySummary[] relays;
-    private String bridges_published;
-    private BridgeSummary[] bridges;
-  }
-
-  private static class RelaySummary {
-    private String n;
-    private String f;
-    private String[] a;
-    private boolean r;
-  }
-
-  private static class BridgeSummary {
-    private String n;
-    private String h;
-    private boolean r;
-  }
-
-  @Test()
-  public void testValidSummaryRelay() throws IOException {
-    this.runTest("/summary");
-    assertEquals("2013-04-24 12:00:00",
-        this.summaryDocument.relays_published);
-    assertEquals(3, this.summaryDocument.relays.length);
-    RelaySummary relay = null;
-    for (RelaySummary r : this.summaryDocument.relays) {
-      if (r.f.equals("000C5F55BD4814B917CC474BD537F1A3B33CCE2A")) {
-        relay = r;
-        break;
-      }
-    }
-    assertNotNull(relay);
-    assertEquals("TorkaZ", relay.n);
-    assertEquals(3, relay.a.length);
-    assertEquals("62.216.201.221", relay.a[0]);
-    assertFalse(relay.r);
-  }
-
-  @Test()
-  public void testValidSummaryBridge() {
-    this.runTest("/summary");
-    assertEquals("2013-04-24 01:07:04",
-        this.summaryDocument.bridges_published);
-    assertEquals(3, this.summaryDocument.bridges.length);
-    BridgeSummary bridge = null;
-    for (BridgeSummary b : this.summaryDocument.bridges) {
-      if (b.h.equals("0000831B236DFF73D409AD17B40E2A728A53994F")) {
-        bridge = b;
-        break;
-      }
-    }
-    assertNotNull(bridge);
-    assertEquals("ec2bridgercc7f31fe", bridge.n);
-    assertFalse(bridge.r);
-  }
-
-  @Test()
-  public void testNonExistantDocumentType() {
-    this.assertErrorStatusCode(
-        "/doesnotexist", 400);
-  }
-
-  @Test()
-  public void testSUMMARYDocument() {
-    this.assertErrorStatusCode(
-        "/SUMMARY", 400);
-  }
-
-  @Test()
-  public void testTypeRelay() {
-    this.assertSummaryDocument(
-        "/summary?type=relay", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testTypeBridge() {
-    this.assertSummaryDocument(
-        "/summary?type=bridge", 0, null, 3, null);
-  }
-
-  @Test()
-  public void testTypeBridgerelay() {
-    this.assertErrorStatusCode(
-        "/summary?type=bridgerelay", 400);
-  }
-
-  @Test()
-  public void testTypeRelayBridge() {
-    this.assertSummaryDocument(
-        "/summary?type=relay&type=bridge", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testTypeBridgeRelay() {
-    this.assertSummaryDocument(
-        "/summary?type=bridge&type=relay", 0, null, 3, null);
-  }
-
-  @Test()
-  public void testTypeRelayRelay() {
-    this.assertSummaryDocument(
-        "/summary?type=relay&type=relay", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testTYPERelay() {
-    this.assertErrorStatusCode(
-        "/summary?TYPE=relay", 400);
-  }
-
-  @Test()
-  public void testTypeRELAY() {
-    this.assertSummaryDocument(
-        "/summary?type=RELAY", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testRunningTrue() {
-    this.assertSummaryDocument(
-        "/summary?running=true", 1, new String[] { "Ferrari458" }, 1,
-        new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testRunningFalse() {
-    this.assertSummaryDocument(
-        "/summary?running=false", 2, null, 2, null);
-  }
-
-  @Test()
-  public void testRunningTruefalse() {
-    this.assertErrorStatusCode(
-        "/summary?running=truefalse", 400);
-  }
-
-  @Test()
-  public void testRunningTrueFalse() {
-    this.assertSummaryDocument(
-        "/summary?running=true&running=false", 1,
-        new String[] { "Ferrari458" }, 1,  new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testRunningFalseTrue() {
-    this.assertSummaryDocument(
-        "/summary?running=false&running=true", 2, null, 2, null);
-  }
-
-  @Test()
-  public void testRunningTrueTrue() {
-    this.assertSummaryDocument(
-        "/summary?running=true&running=true", 1,
-        new String[] { "Ferrari458" }, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testRUNNINGTrue() {
-    this.assertErrorStatusCode(
-        "/summary?RUNNING=true", 400);
-  }
-
-  @Test()
-  public void testRunningTRUE() {
-    this.assertSummaryDocument(
-        "/summary?running=TRUE", 1, null, 1, null);
-  }
-
-  @Test()
-  public void testSearchTorkaZ() {
-    this.assertSummaryDocument(
-        "/summary?search=TorkaZ", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchTorkaX() {
-    this.assertSummaryDocument(
-        "/summary?search=TorkaX", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchOrkaZ() {
-    this.assertSummaryDocument(
-        "/summary?search=orkaZ", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchTorka() {
-    this.assertSummaryDocument(
-        "/summary?search=Torka", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchTORKAZ() {
-    this.assertSummaryDocument(
-        "/summary?search=TORKAZ", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=$000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarFingerprint39() {
-    this.assertSummaryDocument(
-        "/summary?search=$000C5F55BD4814B917CC474BD537F1A3B33CCE2", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarFingerprintLowerCase39() {
-    this.assertSummaryDocument(
-        "/summary?search=$000c5f55bd4814b917cc474bd537f1a3b33cce2", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchFingerprintLowerCase39() {
-    this.assertSummaryDocument(
-        "/summary?search=000c5f55bd4814b917cc474bd537f1a3b33cce2", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94cee", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarHashedFingerprint39() {
-    this.assertSummaryDocument(
-        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94ce", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchDollarHashedFingerprint41() {
-    this.assertErrorStatusCode(
-        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94ceee",
-        400);
-  }
-
-  @Test()
-  public void testSearchBase64FingerprintAlphaNum() {
-    this.assertSummaryDocument(
-        "/summary?search=AAxfVb1IFLkXzEdL1Tfxo7M8zio", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64FingerprintSlash() {
-    this.assertSummaryDocument(
-        "/summary?search=ABwTs6Vacbl3ymXshVOdecZTo/w", 1,
-        new String[] { "Ferrari458" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64FingerprintPlus() {
-    this.assertSummaryDocument(
-        "/summary?search=ACXBNsHzqe7+KuP5GPA7+iG1Bws", 1,
-        new String[] { "TimMayTribute" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64FingerprintBridge() {
-    this.assertSummaryDocument(
-        "/summary?search=AACDGyNt/3PUCa0XtA4qcopTmU8", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64FingerprintPartial() {
-    this.assertSummaryDocument(
-        "/summary?search=AAx", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64HashedFingerprintTorkaZ() {
-    this.assertSummaryDocument(
-        "/summary?search=WqFMCNYpE+AFeprVhjtFjAzpTO4", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchBase64Fingerprint28() {
-    this.assertErrorStatusCode(
-        "/summary?search=AAAAAAAAAAAA//AAAAAAAAAAAAAA", 400);
-  }
-
-  @Test()
-  public void testSearchIp() {
-    this.assertSummaryDocument(
-        "/summary?search=62.216.201.221", 1, new String[] { "TorkaZ" }, 0,
-        null);
-  }
-
-  @Test()
-  public void testSearchIp24Network() {
-    this.assertSummaryDocument(
-        "/summary?search=62.216.201", 1, new String[] { "TorkaZ" }, 0,
-        null);
-  }
-
-  @Test()
-  public void testSearchIpExit() {
-    this.assertSummaryDocument(
-        "/summary?search=62.216.201.222", 1, new String[] { "TorkaZ" }, 0,
-        null);
-  }
-
-  @Test()
-  public void testSearchIpv6() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:4f8:3:2e::51]", 1,
-        new String[] { "Ferrari458" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6Slash64NoTrailingBracket() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:4f8:3:2e::", 1,
-        new String[] { "Ferrari458" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6Slash64TrailingBracket() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:4f8:3:2e::]", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6Uncompressed() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:04f8:0003:002e:0000:0000:0000:0051]", 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6UpperCase() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:4F8:3:2E::51]", 1,
-        new String[] { "Ferrari458" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6ThreeColons() {
-    this.assertSummaryDocument(
-        "/summary?search=[2001:4f8:3:2e:::51]", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6FiveHex() {
-    this.assertSummaryDocument(
-        "/summary?search=[20014:f80:3:2e::51]", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6NineGroups() {
-    this.assertSummaryDocument(
-        "/summary?search=[1:2:3:4:5:6:7:8:9]", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchIpv6TcpPort() {
-    this.assertErrorStatusCode(
-        "/summary?search=[2001:4f8:3:2e::51]:9001", 400);
-  }
-
-  @Test()
-  public void testSearchGummy() {
-    this.assertSummaryDocument(
-        "/summary?search=gummy", 0, null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchGummi() {
-    this.assertSummaryDocument(
-        "/summary?search=gummi", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchUmmy() {
-    this.assertSummaryDocument(
-        "/summary?search=ummy", 0, null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchGumm() {
-    this.assertSummaryDocument(
-        "/summary?search=gumm", 0, null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchGUMMY() {
-    this.assertSummaryDocument(
-        "/summary?search=GUMMY", 0, null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=$1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarHashedFingerprint39() {
-    this.assertSummaryDocument(
-        "/summary?search=$1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB75", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarHashedFingerprintLowerCase39() {
-    this.assertSummaryDocument(
-        "/summary?search=$1fede50ed8dba1dd9f9165f78c8131e4a44ab75", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeHashedFingerprintLowerCase39() {
-    this.assertSummaryDocument(
-        "/summary?search=1fede50ed8dba1dd9f9165f78c8131e4a44ab75", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarHashedHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=$CE52F898DB3678BCE33FAC28C92774DE90D618B5", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarHashedHashedFingerprint39() {
-    this.assertSummaryDocument(
-        "/summary?search=$CE52F898DB3678BCE33FAC28C92774DE90D618B", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testSearchBridgeDollarOriginalFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?search=$0010D49C6DA1E46A316563099F41BFE40B6C7183", 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testSearchUnderscore() {
-    this.assertErrorStatusCode(
-        "/summary?search=_", 400);
-  }
-
-  @Test()
-  public void testSearchTypeRelay() {
-    this.assertSummaryDocument("/summary?search=type:relay", 3, null, 0,
-        null);
-  }
-
-  @Test()
-  public void testSearchTypeRelayTorkaZ() {
-    this.assertSummaryDocument("/summary?search=type:relay TorkaZ", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchTorkaZTypeRelay() {
-    this.assertSummaryDocument("/summary?search=TorkaZ type:relay", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testSearchTypeRelayTypeDirectory() {
-    this.assertSummaryDocument(
-        "/summary?search=type:relay type:directory", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testSearchTypeDirectoryTypeRelay() {
-    this.assertErrorStatusCode(
-        "/summary?search=type:directory type:relay", 400);
-  }
-
-  @Test()
-  public void testSearchFooBar() {
-    this.assertErrorStatusCode("/summary?search=foo:bar", 400);
-  }
-
-  @Test()
-  public void testSearchSearchTorkaZ() {
-    this.assertErrorStatusCode("/summary?search=search:TorkaZ", 400);
-  }
-
-  @Test()
-  public void testSearchLimitOne() {
-    this.assertErrorStatusCode("/summary?search=limit:1", 400);
-  }
-
-  @Test()
-  public void testLookupFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testLookupDollarFingerprint() {
-    this.assertErrorStatusCode(
-        "/summary?lookup=$000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 400);
-  }
-
-  @Test()
-  public void testLookupDollarFingerprint39() {
-    this.assertErrorStatusCode(
-        "/summary?lookup=$000C5F55BD4814B917CC474BD537F1A3B33CCE2", 400);
-  }
-
-  @Test()
-  public void testLookupFingerprintLowerCase39() {
-    this.assertErrorStatusCode(
-        "/summary?lookup=000c5f55bd4814b917cc474bd537f1a3b33cce2", 400);
-  }
-
-  @Test()
-  public void testLookupHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=5aa14c08d62913e0057a9ad5863b458c0ce94cee", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testLookupBridgeHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testLookupBridgeHashedHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=CE52F898DB3678BCE33FAC28C92774DE90D618B5", 0,
-        null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testLookupBridgeOriginalFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=0010D49C6DA1E46A316563099F41BFE40B6C7183", 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testLookupNonExistantFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?lookup=0000000000000000000000000000000000000000", 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFingerprintRelayFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?fingerprint=000C5F55BD4814B917CC474BD537F1A3B33CCE2A",
-        1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testFingerprintRelayHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?fingerprint=5aa14c08d62913e0057a9ad5863b458c0ce94cee",
-        0, null, 0, null);
-  }
-
-  @Test()
-  public void testFingerprintBridgeHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?fingerprint=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756",
-        0, null, 1, new String[] { "gummy" });
-  }
-
-  @Test()
-  public void testFingerprintBridgeHashedHashedFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?fingerprint=CE52F898DB3678BCE33FAC28C92774DE90D618B5",
-        0, null, 0, null);
-  }
-
-  @Test()
-  public void testFingerprintBridgeOriginalFingerprint() {
-    this.assertSummaryDocument(
-        "/summary?fingerprint=0010D49C6DA1E46A316563099F41BFE40B6C7183",
-        0, null, 0, null);
-  }
-
-  @Test()
-  public void testCountryDe() {
-    this.assertSummaryDocument(
-        "/summary?country=de", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testCountryFr() {
-    this.assertSummaryDocument(
-        "/summary?country=fr", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testCountryZz() {
-    this.assertSummaryDocument(
-        "/summary?country=zz", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testCountryDE() {
-    this.assertSummaryDocument(
-        "/summary?country=DE", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testCountryDeu() {
-    this.assertErrorStatusCode(
-        "/summary?country=deu", 400);
-  }
-
-  @Test()
-  public void testCountryD() {
-    this.assertErrorStatusCode(
-        "/summary?country=d", 400);
-  }
-
-  @Test()
-  public void testCountryA1() {
-    this.assertSummaryDocument(
-        "/summary?country=a1", 1, new String[] { "TimMayTribute" }, 0,
-        null);
-  }
-
-  @Test()
-  public void testCountryDeDe() {
-    this.assertSummaryDocument(
-        "/summary?country=de&country=de", 1, new String[] { "TorkaZ" }, 0,
-        null);
-  }
-
-  @Test()
-  public void testAsAS8767() {
-    this.assertSummaryDocument(
-        "/summary?as=AS8767", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testAs8767() {
-    this.assertSummaryDocument(
-        "/summary?as=8767", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testAsAS() {
-    this.assertErrorStatusCode(
-        "/summary?as=AS", 400);
-  }
-
-  @Test()
-  public void testAsas8767() {
-    this.assertSummaryDocument(
-        "/summary?as=as8767", 1, new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testAsASSpace8767() {
-    this.assertErrorStatusCode(
-        "/summary?as=AS 8767", 400);
-  }
-
-  @Test()
-  public void testFlagRunning() {
-    this.assertSummaryDocument(
-        "/summary?flag=Running", 3, null, 1, null);
-  }
-
-  @Test()
-  public void testFlagValid() {
-    this.assertSummaryDocument(
-        "/summary?flag=Valid", 3, null, 3, null);
-  }
-
-  @Test()
-  public void testFlagFast() {
-    this.assertSummaryDocument(
-        "/summary?flag=Fast", 2, null, 0, null);
-  }
-
-  @Test()
-  public void testFlagNamed() {
-    this.assertSummaryDocument(
-        "/summary?flag=Named", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testFlagUnnamed() {
-    this.assertSummaryDocument(
-        "/summary?flag=Unnamed", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testFlagV2Dir() {
-    this.assertSummaryDocument(
-        "/summary?flag=V2Dir", 2, null, 0, null);
-  }
-
-  @Test()
-  public void testFlagGuard() {
-    this.assertSummaryDocument(
-        "/summary?flag=Guard", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testFlagCool() {
-    this.assertSummaryDocument(
-        "/summary?flag=Cool", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysZeroToTwo() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=0-2", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysUpToThree() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=-3", 0, null, 1, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysThree() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=3", 0, null, 1, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysTwoToFive() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=2-5", 0, null, 1, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysSixToSixteen() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=6-16", 2, null, 1, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysNinetysevenOrMore() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=97-", 0, null, 1, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysNinetyeightOrMore() {
-    this.assertSummaryDocument(
-        "/summary?first_seen_days=98-", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysDashDash() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days=--", 400);
-  }
-
-  @Test()
-  public void testFirstSeenDaysDashOneDash() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days=-1-", 400);
-  }
-
-  @Test()
-  public void testFirstSeenDaysZeroDotDotOne() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days=0..1", 400);
-  }
-
-  @Test()
-  public void testFirstSeenDaysElevenDigits() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days=12345678901", 400);
-  }
-
-  @Test()
-  public void testFirstSeenDaysLargeTenDigitNumber() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days=9999999999", 400);
-  }
-
-  @Test()
-  public void testFirstSeenDaysMaxInt() {
-    this.assertSummaryDocument(
-        "/summary?last_seen_days=" + String.valueOf(Integer.MAX_VALUE), 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFirstSeenDaysMaxIntPlusOne() {
-    this.assertErrorStatusCode(
-        "/summary?first_seen_days="
-        + String.valueOf(Integer.MAX_VALUE + 1), 400);
-  }
-
-  @Test()
-  public void testLastSeenDaysZero() {
-    this.assertSummaryDocument(
-        "/summary?last_seen_days=0", 1, null, 1, null);
-  }
-
-  @Test()
-  public void testLastSeenDaysUpToZero() {
-    this.assertSummaryDocument(
-        "/summary?last_seen_days=-0", 1, null, 1, null);
-  }
-
-  @Test()
-  public void testLastSeenDaysOneToThree() {
-    this.assertSummaryDocument(
-        "/summary?last_seen_days=1-3", 1, null, 2, null);
-  }
-
-  @Test()
-  public void testLastSeenDaysSixOrMore() {
-    this.assertSummaryDocument(
-        "/summary?last_seen_days=6-", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testContactSteven() {
-    this.assertSummaryDocument(
-        "/summary?contact=Steven", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testContactStevenMurdoch() {
-    this.assertSummaryDocument(
-        "/summary?contact=Steven Murdoch", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testContactMurdochSteven() {
-    this.assertSummaryDocument(
-        "/summary?contact=Murdoch Steven", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testContactStevenDotMurdoch() {
-    this.assertSummaryDocument(
-        "/summary?contact=Steven.Murdoch", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testContactFbTokenFive() {
-    this.assertSummaryDocument(
-        "/summary?contact=<fb-token:5sR_K_zs2wM=>", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testContactFbToken() {
-    this.assertSummaryDocument(
-        "/summary?contact=<fb-token:", 2, null, 0, null);
-  }
-
-  @Test()
-  public void testContactDash() {
-    this.assertSummaryDocument(
-        "/summary?contact=-", 2, null, 0, null);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightAscending() {
-    this.assertSummaryDocument(
-        "/summary?order=consensus_weight", 3,
-        new String[] { "TorkaZ", "TimMayTribute", "Ferrari458" }, 3,
-        null);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightDescending() {
-    this.assertSummaryDocument(
-        "/summary?order=-consensus_weight", 3,
-        new String[] { "Ferrari458", "TimMayTribute", "TorkaZ" }, 3,
-        null);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightAscendingTwice() {
-    this.assertErrorStatusCode(
-        "/summary?order=consensus_weight,consensus_weight", 400);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightAscendingThenDescending() {
-    this.assertErrorStatusCode(
-        "/summary?order=consensus_weight,-consensus_weight", 400);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightThenNickname() {
-    this.assertErrorStatusCode(
-        "/summary?order=consensus_weight,nickname", 400);
-  }
-
-  @Test()
-  public void testOrderCONSENSUS_WEIGHT() {
-    this.assertSummaryDocument(
-        "/summary?order=CONSENSUS_WEIGHT", 3,
-        new String[] { "TorkaZ", "TimMayTribute", "Ferrari458" }, 3,
-        null);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightAscendingLimit1() {
-    this.assertSummaryDocument(
-        "/summary?order=consensus_weight&limit=1", 1,
-        new String[] { "TorkaZ" }, 0, null);
-  }
-
-  @Test()
-  public void testOrderConsensusWeightDecendingLimit1() {
-    this.assertSummaryDocument(
-        "/summary?order=-consensus_weight&limit=1", 1,
-        new String[] { "Ferrari458" }, 0, null);
-  }
-
-  @Test()
-  public void testOffsetOne() {
-    this.assertSummaryDocument(
-        "/summary?offset=1", 2, null, 3, null);
-  }
-
-  @Test()
-  public void testOffsetAllRelays() {
-    this.assertSummaryDocument(
-        "/summary?offset=3", 0, null, 3, null);
-  }
-
-  @Test()
-  public void testOffsetAllRelaysAndOneBridge() {
-    this.assertSummaryDocument(
-        "/summary?offset=4", 0, null, 2, null);
-  }
-
-  @Test()
-  public void testOffsetAllRelaysAndAllBridges() {
-    this.assertSummaryDocument(
-        "/summary?offset=6", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testOffsetMoreThanAllRelaysAndAllBridges() {
-    this.assertSummaryDocument(
-        "/summary?offset=7", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testOffsetZero() {
-    this.assertSummaryDocument(
-        "/summary?offset=0", 3, null, 3, null);
-  }
-
-  @Test()
-  public void testOffsetMinusOne() {
-    this.assertSummaryDocument(
-        "/summary?offset=-1", 3, null, 3, null);
-  }
-
-  @Test()
-  public void testOffsetOneWord() {
-    this.assertErrorStatusCode(
-        "/summary?offset=one", 400);
-  }
-
-  @Test()
-  public void testLimitOne() {
-    this.assertSummaryDocument(
-        "/summary?limit=1", 1, null, 0, null);
-  }
-
-  @Test()
-  public void testLimitAllRelays() {
-    this.assertSummaryDocument(
-        "/summary?limit=3", 3, null, 0, null);
-  }
-
-  @Test()
-  public void testLimitAllRelaysAndOneBridge() {
-    this.assertSummaryDocument(
-        "/summary?limit=4", 3, null, 1, null);
-  }
-
-  @Test()
-  public void testLimitAllRelaysAndAllBridges() {
-    this.assertSummaryDocument(
-        "/summary?limit=6", 3, null, 3, null);
-  }
-
-  @Test()
-  public void testLimitMoreThanAllRelaysAndAllBridges() {
-    this.assertSummaryDocument(
-        "/summary?limit=7", 3, null, 3, null);
-  }
-
-  @Test()
-  public void testLimitZero() {
-    this.assertSummaryDocument(
-        "/summary?limit=0", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testLimitMinusOne() {
-    this.assertSummaryDocument(
-        "/summary?limit=-1", 0, null, 0, null);
-  }
-
-  @Test()
-  public void testLimitOneWord() {
-    this.assertErrorStatusCode(
-        "/summary?limit=one", 400);
-  }
-
-  @Test()
-  public void testFamilyTorkaZ() {
-    this.assertSummaryDocument(
-        "/summary?family=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 2,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFamilyFerrari458() {
-    this.assertSummaryDocument(
-        "/summary?family=001C13B3A55A71B977CA65EC85539D79C653A3FC", 2,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFamilyTimMayTribute() {
-    this.assertSummaryDocument(
-        "/summary?family=0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", 1,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFamilyBridgegummy() {
-    this.assertSummaryDocument(
-        "/summary?family=0000831B236DFF73D409AD17B40E2A728A53994F", 0,
-        null, 0, null);
-  }
-
-  @Test()
-  public void testFamily39Characters() {
-    this.assertErrorStatusCode(
-        "/summary?family=00000000000000000000000000000000000000", 400);
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/UptimeDocumentWriterTest.java b/src/test/java/org/torproject/onionoo/UptimeDocumentWriterTest.java
deleted file mode 100644
index 8f7fff0..0000000
--- a/src/test/java/org/torproject/onionoo/UptimeDocumentWriterTest.java
+++ /dev/null
@@ -1,260 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-import java.util.List;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.torproject.onionoo.docs.DateTimeHelper;
-import org.torproject.onionoo.docs.DocumentStoreFactory;
-import org.torproject.onionoo.docs.GraphHistory;
-import org.torproject.onionoo.docs.UptimeDocument;
-import org.torproject.onionoo.docs.UptimeStatus;
-import org.torproject.onionoo.updater.DescriptorSourceFactory;
-import org.torproject.onionoo.util.TimeFactory;
-import org.torproject.onionoo.writer.UptimeDocumentWriter;
-
-public class UptimeDocumentWriterTest {
-
-  private static final long TEST_TIME = DateTimeHelper.parse(
-      "2014-03-23 12:00:00");
-
-  private DummyTime dummyTime;
-
-  @Before
-  public void createDummyTime() {
-    this.dummyTime = new DummyTime(TEST_TIME);
-    TimeFactory.setTime(this.dummyTime);
-  }
-
-  private DummyDescriptorSource descriptorSource;
-
-  @Before
-  public void createDummyDescriptorSource() {
-    this.descriptorSource = new DummyDescriptorSource();
-    DescriptorSourceFactory.setDescriptorSource(this.descriptorSource);
-  }
-
-  private DummyDocumentStore documentStore;
-
-  @Before
-  public void createDummyDocumentStore() {
-    this.documentStore = new DummyDocumentStore();
-    DocumentStoreFactory.setDocumentStore(this.documentStore);
-  }
-
-  @Test
-  public void testNoStatuses() {
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    writer.writeDocuments();
-    assertEquals("Without providing any data, nothing should be written "
-        + "to disk.", 0,
-        this.documentStore.getPerformedStoreOperations());
-  }
-
-  private static final String ALL_RELAYS_FINGERPRINT = null;
-
-  private static final String GABELMOO_FINGERPRINT =
-      "F2044413DAC2E02E3D6BCF4735A19BCA1DE97281";
-
-  private void addStatusOneWeekSample(String allRelaysUptime,
-      String gabelmooUptime) {
-    UptimeStatus status = new UptimeStatus();
-    status.setFromDocumentString(allRelaysUptime);
-    this.documentStore.addDocument(status, ALL_RELAYS_FINGERPRINT);
-    status = new UptimeStatus();
-    status.setFromDocumentString(gabelmooUptime);
-    this.documentStore.addDocument(status, GABELMOO_FINGERPRINT);
-  }
-
-  private static final long ONE_SECOND = 1000L,
-      ONE_HOUR = 60L * 60L * ONE_SECOND, FOUR_HOURS = 4L * ONE_HOUR;
-
-  private void assertOneWeekGraph(UptimeDocument document, int graphs,
-      String first, String last, int count, List<Integer> values) {
-    this.assertGraph(document, graphs, "1_week", first, last,
-        (int) (ONE_HOUR / ONE_SECOND), count, values);
-  }
-
-  private void assertOneMonthGraph(UptimeDocument document, int graphs,
-      String first, String last, int count, List<Integer> values) {
-    this.assertGraph(document, graphs, "1_month", first, last,
-        (int) (FOUR_HOURS / ONE_SECOND), count, values);
-  }
-
-  private void assertGraph(UptimeDocument document, int graphs,
-      String graphName, String first, String last, int interval,
-      int count, List<Integer> values) {
-    assertEquals("Should contain exactly " + graphs + " graphs.", graphs,
-        document.getUptime().size());
-    assertTrue("Should contain a graph for " + graphName + ".",
-        document.getUptime().containsKey(graphName));
-    GraphHistory history = document.getUptime().get(graphName);
-    assertEquals("First data point should be " + first + ".",
-        DateTimeHelper.parse(first), history.getFirst());
-    assertEquals("Last data point should be " + last + ".",
-        DateTimeHelper.parse(last), history.getLast());
-    assertEquals("Interval should be " + interval + " seconds.", interval,
-        (int) history.getInterval());
-    assertEquals("Factor should be 1.0 / 999.0.", 1.0 / 999.0,
-        (double) history.getFactor(), 0.01);
-    assertEquals("There should be one data point per hour.", count,
-        (int) history.getCount());
-    assertEquals("Count should be the same as the number of values.",
-        count, history.getValues().size());
-    if (values == null) {
-      for (int value : history.getValues()) {
-        assertEquals("All values should be 999.", 999, value);
-      }
-    } else {
-      assertEquals("Values are not as expected.", values,
-          history.getValues());
-    }
-  }
-
-  @Test
-  public void testOneHourUptime() {
-    this.addStatusOneWeekSample("r 2014-03-23-11 1\n",
-        "r 2014-03-23-11 1\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    assertEquals("Should not contain any graph.", 0,
-        document.getUptime().size());
-  }
-
-  @Test
-  public void testTwoHoursUptime() {
-    this.addStatusOneWeekSample("r 2014-03-23-10 2\n",
-        "r 2014-03-23-10 2\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneWeekGraph(document, 1, "2014-03-23 10:30:00",
-        "2014-03-23 11:30:00", 2, null);
-  }
-
-  @Test
-  public void testTwoHoursUptimeSeparatedByNull() {
-    this.addStatusOneWeekSample("r 2014-03-23-09 1\nr 2014-03-23-11 1\n",
-        "r 2014-03-23-09 1\nr 2014-03-23-11 1\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    assertEquals("Should not contain any graph.", 0,
-        document.getUptime().size());
-  }
-
-  @Test
-  public void testTwoHoursUptimeSeparatedByZero() {
-    this.addStatusOneWeekSample("r 2014-03-23-09 3\n",
-        "r 2014-03-23-09 1\nr 2014-03-23-11 1\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneWeekGraph(document, 1, "2014-03-23 09:30:00",
-        "2014-03-23 11:30:00", 3,
-        Arrays.asList(new Integer[] { 999, 0, 999 }));
-  }
-
-  @Test
-  public void testTwoHoursUptimeThenDowntime() {
-    this.addStatusOneWeekSample("r 2014-03-23-09 3\n",
-        "r 2014-03-23-09 2\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneWeekGraph(document, 1, "2014-03-23 09:30:00",
-        "2014-03-23 11:30:00", 3,
-        Arrays.asList(new Integer[] { 999, 999, 0 }));
-  }
-
-  @Test
-  public void testOneWeekUptime() {
-    this.addStatusOneWeekSample("r 2014-03-16-12 168\n",
-        "r 2014-03-16-12 168\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneWeekGraph(document, 1, "2014-03-16 12:30:00",
-        "2014-03-23 11:30:00", 168, null);
-  }
-
-  @Test
-  public void testOneWeekOneHourUptime() {
-    this.addStatusOneWeekSample("r 2014-03-16-11 169\n",
-        "r 2014-03-16-11 169\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneWeekGraph(document, 2, "2014-03-16 12:30:00",
-        "2014-03-23 11:30:00", 168, null);
-    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
-        "2014-03-23 10:00:00", 43, null);
-  }
-
-  @Test
-  public void testOneMonthPartialIntervalOnline() {
-    this.addStatusOneWeekSample("r 2014-03-16-08 8\n",
-        "r 2014-03-16-11 5\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
-        "2014-03-16 14:00:00", 2, null);
-  }
-
-  @Test
-  public void testOneMonthPartialIntervalOnOff() {
-    this.addStatusOneWeekSample("r 2014-03-16-08 8\n",
-        "r 2014-03-16-10 1\nr 2014-03-16-12 1\n");
-    UptimeDocumentWriter writer = new UptimeDocumentWriter();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    writer.writeDocuments();
-    assertEquals("Should write exactly one document.", 1,
-        this.documentStore.getPerformedStoreOperations());
-    UptimeDocument document = this.documentStore.getDocument(
-        UptimeDocument.class, GABELMOO_FINGERPRINT);
-    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
-        "2014-03-16 14:00:00", 2,
-        Arrays.asList(new Integer[] { 499, 249 }));
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/UptimeStatusUpdaterTest.java b/src/test/java/org/torproject/onionoo/UptimeStatusUpdaterTest.java
deleted file mode 100644
index b418879..0000000
--- a/src/test/java/org/torproject/onionoo/UptimeStatusUpdaterTest.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/* Copyright 2014 The Tor Project
- * See LICENSE for licensing information */
-package org.torproject.onionoo;
-
-import static org.junit.Assert.assertEquals;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.torproject.onionoo.docs.DateTimeHelper;
-import org.torproject.onionoo.docs.DocumentStoreFactory;
-import org.torproject.onionoo.docs.UptimeHistory;
-import org.torproject.onionoo.docs.UptimeStatus;
-import org.torproject.onionoo.updater.DescriptorSourceFactory;
-import org.torproject.onionoo.updater.DescriptorType;
-import org.torproject.onionoo.updater.UptimeStatusUpdater;
-
-public class UptimeStatusUpdaterTest {
-
-  private DummyDescriptorSource descriptorSource;
-
-  @Before
-  public void createDummyDescriptorSource() {
-    this.descriptorSource = new DummyDescriptorSource();
-    DescriptorSourceFactory.setDescriptorSource(this.descriptorSource);
-  }
-
-  private DummyDocumentStore documentStore;
-
-  @Before
-  public void createDummyDocumentStore() {
-    this.documentStore = new DummyDocumentStore();
-    DocumentStoreFactory.setDocumentStore(this.documentStore);
-  }
-
-  @Test
-  public void testNoDescriptorsNoStatusFiles() {
-    UptimeStatusUpdater updater = new UptimeStatusUpdater();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    updater.updateStatuses();
-    assertEquals("Without providing any data, nothing should be written "
-        + "to disk.", 0,
-        this.documentStore.getPerformedStoreOperations());
-  }
-
-  private static final long VALID_AFTER_SAMPLE =
-      DateTimeHelper.parse("2014-03-21 20:00:00");
-
-  private static final String GABELMOO_FINGERPRINT =
-      "F2044413DAC2E02E3D6BCF4735A19BCA1DE97281";
-
-  private void addConsensusSample() {
-    DummyStatusEntry statusEntry = new DummyStatusEntry(
-        GABELMOO_FINGERPRINT);
-    statusEntry.addFlag("Running");
-    DummyConsensus consensus = new DummyConsensus();
-    consensus.setValidAfterMillis(VALID_AFTER_SAMPLE);
-    consensus.addStatusEntry(statusEntry);
-    this.descriptorSource.addDescriptor(DescriptorType.RELAY_CONSENSUSES,
-        consensus);
-  }
-
-  @Test
-  public void testOneConsensusNoStatusFiles() {
-    this.addConsensusSample();
-    UptimeStatusUpdater updater = new UptimeStatusUpdater();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    updater.updateStatuses();
-    assertEquals("Two status files should have been written to disk.",
-        2, this.documentStore.getPerformedStoreOperations());
-    for (String fingerprint : new String[] { GABELMOO_FINGERPRINT,
-        null }) {
-      UptimeStatus status = this.documentStore.getDocument(
-          UptimeStatus.class, fingerprint);
-      UptimeHistory history = status.getRelayHistory().first();
-      assertEquals("History must contain one entry.", 1,
-          status.getRelayHistory().size());
-      assertEquals("History not for relay.", true, history.isRelay());
-      assertEquals("History start millis not as expected.",
-          VALID_AFTER_SAMPLE, history.getStartMillis());
-      assertEquals("History uptime hours must be 1.", 1,
-          history.getUptimeHours());
-    }
-  }
-
-  private static final String ALL_RELAYS_AND_BRIDGES_FINGERPRINT = null;
-
-  private static final String ALL_RELAYS_AND_BRIDGES_UPTIME_SAMPLE =
-      "r 2013-07-22-17 5811\n" /* ends 2014-03-21 20:00:00 */
-      + "b 2013-07-22-17 5811\n"; /* ends 2014-03-21 20:00:00 */
-
-  private void addAllRelaysAndBridgesUptimeSample() {
-    UptimeStatus uptimeStatus = new UptimeStatus();
-    uptimeStatus.setFromDocumentString(
-        ALL_RELAYS_AND_BRIDGES_UPTIME_SAMPLE);
-    this.documentStore.addDocument(uptimeStatus,
-        ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
-  }
-
-  @Test
-  public void testOneConsensusOneStatusFiles() {
-    this.addAllRelaysAndBridgesUptimeSample();
-    this.addConsensusSample();
-    UptimeStatusUpdater updater = new UptimeStatusUpdater();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    updater.updateStatuses();
-    assertEquals("Two status files should have been written to disk.",
-        2, this.documentStore.getPerformedStoreOperations());
-    UptimeStatus status = this.documentStore.getDocument(
-        UptimeStatus.class, ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
-    assertEquals("Relay history must contain one entry", 1,
-        status.getRelayHistory().size());
-    UptimeHistory history = status.getRelayHistory().first();
-    assertEquals("History not for relay.", true, history.isRelay());
-    assertEquals("History start millis not as expected.",
-        DateTimeHelper.parse("2013-07-22 17:00:00"),
-        history.getStartMillis());
-    assertEquals("History uptime hours must be 5812.", 5812,
-        history.getUptimeHours());
-  }
-
-  private static final long PUBLISHED_SAMPLE =
-      DateTimeHelper.parse("2014-03-21 20:37:03");
-
-  private static final String NDNOP2_FINGERPRINT =
-      "DE6397A047ABE5F78B4C87AF725047831B221AAB";
-
-  private void addBridgeStatusSample() {
-    DummyStatusEntry statusEntry = new DummyStatusEntry(
-        NDNOP2_FINGERPRINT);
-    statusEntry.addFlag("Running");
-    DummyBridgeStatus bridgeStatus = new DummyBridgeStatus();
-    bridgeStatus.setPublishedMillis(PUBLISHED_SAMPLE);
-    bridgeStatus.addStatusEntry(statusEntry);
-    this.descriptorSource.addDescriptor(DescriptorType.BRIDGE_STATUSES,
-        bridgeStatus);
-  }
-
-  @Test
-  public void testOneBridgeStatusNoStatusFiles() {
-    this.addBridgeStatusSample();
-    UptimeStatusUpdater updater = new UptimeStatusUpdater();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    updater.updateStatuses();
-    assertEquals("Two status files should have been written to disk.",
-        2, this.documentStore.getPerformedStoreOperations());
-    for (String fingerprint : new String[] { NDNOP2_FINGERPRINT,
-        null }) {
-      UptimeStatus status = this.documentStore.getDocument(
-          UptimeStatus.class, fingerprint);
-      UptimeHistory history = status.getBridgeHistory().first();
-      assertEquals("Bridge history must contain one entry.", 1,
-          status.getBridgeHistory().size());
-      assertEquals("History not for bridge.", false, history.isRelay());
-      assertEquals("History start millis not as expected.",
-          DateTimeHelper.parse("2014-03-21 20:00:00"),
-          history.getStartMillis());
-      assertEquals("History uptime hours must be 1.", 1,
-          history.getUptimeHours());
-    }
-  }
-
-  @Test
-  public void testOneBridgeStatusOneStatusFiles() {
-    this.addAllRelaysAndBridgesUptimeSample();
-    this.addBridgeStatusSample();
-    UptimeStatusUpdater updater = new UptimeStatusUpdater();
-    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
-    updater.updateStatuses();
-    assertEquals("Two status files should have been written to disk.",
-        2, this.documentStore.getPerformedStoreOperations());
-    UptimeStatus status = this.documentStore.getDocument(
-        UptimeStatus.class, ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
-    assertEquals("Bridge history must contain one entry.", 1,
-        status.getBridgeHistory().size());
-    UptimeHistory history = status.getBridgeHistory().last();
-    assertEquals("History not for bridge.", false, history.isRelay());
-    assertEquals("History start millis not as expected.",
-        DateTimeHelper.parse("2013-07-22 17:00:00"),
-        history.getStartMillis());
-    assertEquals("History uptime hours must be 5812.", 5812,
-        history.getUptimeHours());
-  }
-}
-
diff --git a/src/test/java/org/torproject/onionoo/docs/BandwidthStatusTest.java b/src/test/java/org/torproject/onionoo/docs/BandwidthStatusTest.java
index b943d34..96988d1 100644
--- a/src/test/java/org/torproject/onionoo/docs/BandwidthStatusTest.java
+++ b/src/test/java/org/torproject/onionoo/docs/BandwidthStatusTest.java
@@ -12,7 +12,7 @@ import java.util.TreeMap;
 import org.junit.Before;
 import org.junit.Test;
 import org.torproject.descriptor.BandwidthHistory;
-import org.torproject.onionoo.DummyTime;
+import org.torproject.onionoo.util.DummyTime;
 import org.torproject.onionoo.util.TimeFactory;
 
 public class BandwidthStatusTest {
diff --git a/src/test/java/org/torproject/onionoo/docs/DummyDocumentStore.java b/src/test/java/org/torproject/onionoo/docs/DummyDocumentStore.java
new file mode 100644
index 0000000..aa987a1
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/docs/DummyDocumentStore.java
@@ -0,0 +1,120 @@
+package org.torproject.onionoo.docs;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.torproject.onionoo.docs.Document;
+import org.torproject.onionoo.docs.DocumentStore;
+
+public class DummyDocumentStore extends DocumentStore {
+
+  private Map<Class<? extends Document>, SortedMap<String, Document>>
+      storedDocuments = new HashMap<Class<? extends Document>,
+      SortedMap<String, Document>>();
+
+  private static final String FINGERPRINT_NULL = "";
+
+  private <T extends Document> SortedMap<String, Document>
+      getStoredDocumentsByClass(Class<T> documentType) {
+    if (!this.storedDocuments.containsKey(documentType)) {
+      this.storedDocuments.put(documentType,
+          new TreeMap<String, Document>());
+    }
+    return this.storedDocuments.get(documentType);
+  }
+
+  public <T extends Document> void addDocument(T document,
+      String fingerprint) {
+    this.getStoredDocumentsByClass(document.getClass()).put(
+        fingerprint == null ? FINGERPRINT_NULL : fingerprint, document);
+  }
+
+  public <T extends Document> T getDocument(Class<T> documentType,
+      String fingerprint) {
+    return documentType.cast(this.getStoredDocumentsByClass(documentType).
+        get(fingerprint == null ? FINGERPRINT_NULL : fingerprint));
+  }
+
+  public void flushDocumentCache() {
+    /* Nothing to do. */
+  }
+
+  public String getStatsString() {
+    /* No statistics to return. */
+    return null;
+  }
+
+  private int performedListOperations = 0;
+  public int getPerformedListOperations() {
+    return this.performedListOperations;
+  }
+
+  public <T extends Document> SortedSet<String> list(
+      Class<T> documentType, long modifiedAfter) {
+    return this.list(documentType);
+  }
+
+  public <T extends Document> SortedSet<String> list(
+      Class<T> documentType) {
+    this.performedListOperations++;
+    SortedSet<String> fingerprints = new TreeSet<String>(
+        this.getStoredDocumentsByClass(documentType).keySet());
+    fingerprints.remove(FINGERPRINT_NULL);
+    return fingerprints;
+  }
+
+  private int performedRemoveOperations = 0;
+  public int getPerformedRemoveOperations() {
+    return this.performedRemoveOperations;
+  }
+
+  public <T extends Document> boolean remove(Class<T> documentType) {
+    return this.remove(documentType, null);
+  }
+
+  public <T extends Document> boolean remove(Class<T> documentType,
+      String fingerprint) {
+    this.performedRemoveOperations++;
+    return this.getStoredDocumentsByClass(documentType).remove(
+        fingerprint) != null;
+  }
+
+  private int performedRetrieveOperations = 0;
+  public int getPerformedRetrieveOperations() {
+    return this.performedRetrieveOperations;
+  }
+
+  public <T extends Document> T retrieve(Class<T> documentType,
+      boolean parse) {
+    return this.retrieve(documentType, parse, null);
+  }
+
+  public <T extends Document> T retrieve(Class<T> documentType,
+      boolean parse, String fingerprint) {
+    this.performedRetrieveOperations++;
+    return documentType.cast(this.getStoredDocumentsByClass(documentType).
+        get(fingerprint == null ? FINGERPRINT_NULL : fingerprint));
+  }
+
+  private int performedStoreOperations = 0;
+  public int getPerformedStoreOperations() {
+    return this.performedStoreOperations;
+  }
+
+  public <T extends Document> boolean store(T document) {
+    return this.store(document, null);
+  }
+
+  public <T extends Document> boolean store(T document,
+      String fingerprint) {
+    this.performedStoreOperations++;
+    this.getStoredDocumentsByClass(document.getClass()).put(
+        fingerprint == null ? FINGERPRINT_NULL : fingerprint, document);
+    return true;
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java b/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java
new file mode 100644
index 0000000..83ffa5f
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/server/ResourceServletTest.java
@@ -0,0 +1,1385 @@
+/* Copyright 2013 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.onionoo.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.torproject.onionoo.docs.DateTimeHelper;
+import org.torproject.onionoo.docs.DocumentStoreFactory;
+import org.torproject.onionoo.docs.DummyDocumentStore;
+import org.torproject.onionoo.docs.UpdateStatus;
+import org.torproject.onionoo.server.HttpServletRequestWrapper;
+import org.torproject.onionoo.server.HttpServletResponseWrapper;
+import org.torproject.onionoo.server.NodeIndexer;
+import org.torproject.onionoo.server.NodeIndexerFactory;
+import org.torproject.onionoo.server.ResourceServlet;
+import org.torproject.onionoo.util.DummyTime;
+import org.torproject.onionoo.util.Time;
+import org.torproject.onionoo.util.TimeFactory;
+
+import com.google.gson.Gson;
+
+/* TODO This test class could (should?) be split into ResponseBuilderTest
+ * which tests ResponseBuilder and a much shorter ResourceServletTest
+ * which tests servlet specifics. */
+public class ResourceServletTest {
+
+  private SortedMap<String, org.torproject.onionoo.docs.SummaryDocument>
+      relays, bridges;
+
+  private long currentTimeMillis = DateTimeHelper.parse(
+      "2013-04-24 12:22:22");
+
+  private class TestingHttpServletRequestWrapper
+      extends HttpServletRequestWrapper {
+    private String requestURI;
+    private String queryString;
+    private Map<String, String[]> parameterMap;
+    private TestingHttpServletRequestWrapper(String requestURI,
+        String queryString, Map<String, String[]> parameterMap) {
+      super(null);
+      this.requestURI = requestURI;
+      this.queryString = queryString;
+      this.parameterMap = parameterMap == null
+          ? new HashMap<String, String[]>() : parameterMap;
+    }
+    protected String getRequestURI() {
+      return this.requestURI;
+    }
+    @SuppressWarnings("rawtypes")
+    protected Map getParameterMap() {
+      return this.parameterMap;
+    }
+    protected String[] getParameterValues(String parameterKey) {
+      return this.parameterMap.get(parameterKey);
+    }
+    protected String getQueryString() {
+      return this.queryString;
+    }
+  }
+
+  private class TestingHttpServletResponseWrapper extends
+      HttpServletResponseWrapper {
+    private TestingHttpServletResponseWrapper() {
+      super(null);
+    }
+    private int errorStatusCode;
+    protected void sendError(int errorStatusCode) throws IOException {
+      this.errorStatusCode = errorStatusCode;
+    }
+    private Map<String, String> headers = new HashMap<String, String>();
+    protected void setHeader(String headerName, String headerValue) {
+      this.headers.put(headerName, headerValue);
+    }
+    protected void setContentType(String contentType) {
+    }
+    protected void setCharacterEncoding(String characterEncoding) {
+    }
+    private StringWriter stringWriter;
+    protected PrintWriter getWriter() throws IOException {
+      if (this.stringWriter == null) {
+        this.stringWriter = new StringWriter();
+        return new PrintWriter(this.stringWriter);
+      } else {
+        throw new IOException("Can only request writer once");
+      }
+    }
+    private String getWrittenContent() {
+      return this.stringWriter == null ? null
+          : this.stringWriter.toString();
+    }
+  }
+
+  private TestingHttpServletRequestWrapper request;
+
+  private TestingHttpServletResponseWrapper response;
+
+  private String responseString;
+
+  private SummaryDocument summaryDocument;
+
+  @Before
+  public void createSampleRelaysAndBridges() {
+    org.torproject.onionoo.docs.SummaryDocument relayTorkaZ =
+        new org.torproject.onionoo.docs.SummaryDocument(true, "TorkaZ",
+        "000C5F55BD4814B917CC474BD537F1A3B33CCE2A", Arrays.asList(
+        new String[] { "62.216.201.221", "62.216.201.222",
+        "62.216.201.223" }), DateTimeHelper.parse("2013-04-19 05:00:00"),
+        false, new TreeSet<String>(Arrays.asList(new String[] { "Running",
+        "Valid" })), 20L, "de",
+        DateTimeHelper.parse("2013-04-18 05:00:00"), "AS8767",
+        "torkaz <klaus dot zufall at gmx dot de> "
+        + "<fb-token:np5_g_83jmf=>", new TreeSet<String>(Arrays.asList(
+        new String[] { "001C13B3A55A71B977CA65EC85539D79C653A3FC",
+        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B" })));
+    org.torproject.onionoo.docs.SummaryDocument relayFerrari458 =
+        new org.torproject.onionoo.docs.SummaryDocument(true, "Ferrari458",
+        "001C13B3A55A71B977CA65EC85539D79C653A3FC", Arrays.asList(
+        new String[] { "68.38.171.200", "[2001:4f8:3:2e::51]" }),
+        DateTimeHelper.parse("2013-04-24 12:00:00"), true,
+        new TreeSet<String>(Arrays.asList(new String[] { "Fast", "Named",
+        "Running", "V2Dir", "Valid" })), 1140L, "us",
+        DateTimeHelper.parse("2013-02-12 16:00:00"), "AS7922", null,
+        new TreeSet<String>(Arrays.asList(new String[] {
+        "000C5F55BD4814B917CC474BD537F1A3B33CCE2A" })));
+    org.torproject.onionoo.docs.SummaryDocument relayTimMayTribute =
+        new org.torproject.onionoo.docs.SummaryDocument(true, "TimMayTribute",
+        "0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", Arrays.asList(
+        new String[] { "89.69.68.246" }),
+        DateTimeHelper.parse("2013-04-22 20:00:00"), false,
+        new TreeSet<String>(Arrays.asList(new String[] { "Fast",
+            "Running", "Unnamed", "V2Dir", "Valid" })), 63L, "a1",
+        DateTimeHelper.parse("2013-04-16 18:00:00"), "AS6830",
+        "1024D/51E2A1C7 steven j. murdoch "
+        + "<tor+steven.murdoch at cl.cam.ac.uk> <fb-token:5sr_k_zs2wm=>",
+        new TreeSet<String>());
+    org.torproject.onionoo.docs.SummaryDocument bridgeec2bridgercc7f31fe =
+        new org.torproject.onionoo.docs.SummaryDocument(false,
+        "ec2bridgercc7f31fe", "0000831B236DFF73D409AD17B40E2A728A53994F",
+        Arrays.asList(new String[] { "10.199.7.176" }),
+        DateTimeHelper.parse("2013-04-21 18:07:03"), false,
+        new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
+        null, DateTimeHelper.parse("2013-04-20 15:37:04"), null, null,
+        null);
+    org.torproject.onionoo.docs.SummaryDocument bridgeUnnamed =
+        new org.torproject.onionoo.docs.SummaryDocument(false, "Unnamed",
+        "0002D9BDBBC230BD9C78FF502A16E0033EF87E0C", Arrays.asList(
+        new String[] { "10.0.52.84" }),
+        DateTimeHelper.parse("2013-04-20 17:37:04"), false,
+        new TreeSet<String>(Arrays.asList(new String[] { "Valid" })), -1L,
+        null, DateTimeHelper.parse("2013-04-14 07:07:05"), null, null,
+        null);
+    org.torproject.onionoo.docs.SummaryDocument bridgegummy =
+        new org.torproject.onionoo.docs.SummaryDocument(false, "gummy",
+        "1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", Arrays.asList(
+        new String[] { "10.63.169.98" }),
+        DateTimeHelper.parse("2013-04-24 01:07:04"), true,
+        new TreeSet<String>(Arrays.asList(new String[] { "Running",
+        "Valid" })), -1L, null,
+        DateTimeHelper.parse("2013-01-16 21:07:04"), null, null, null);
+    this.relays =
+        new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>();
+    this.relays.put("000C5F55BD4814B917CC474BD537F1A3B33CCE2A",
+        relayTorkaZ);
+    this.relays.put("001C13B3A55A71B977CA65EC85539D79C653A3FC",
+        relayFerrari458);
+    this.relays.put("0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B",
+        relayTimMayTribute);
+    this.bridges =
+        new TreeMap<String, org.torproject.onionoo.docs.SummaryDocument>();
+    this.bridges.put("0000831B236DFF73D409AD17B40E2A728A53994F",
+        bridgeec2bridgercc7f31fe);
+    this.bridges.put("0002D9BDBBC230BD9C78FF502A16E0033EF87E0C",
+        bridgeUnnamed);
+    this.bridges.put("1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756",
+        bridgegummy);
+  }
+
+  private void runTest(String request) {
+    try {
+      this.createDummyTime();
+      this.createDummyDocumentStore();
+      this.createNodeIndexer();
+      this.makeRequest(request);
+      this.parseResponse();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void createDummyTime() {
+    Time dummyTime = new DummyTime(this.currentTimeMillis);
+    TimeFactory.setTime(dummyTime);
+  }
+
+  private void createDummyDocumentStore() {
+    DummyDocumentStore documentStore = new DummyDocumentStore();
+    UpdateStatus updateStatus = new UpdateStatus();
+    updateStatus.setUpdatedMillis(this.currentTimeMillis);
+    documentStore.addDocument(updateStatus, null);
+    for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e :
+        this.relays.entrySet()) {
+      documentStore.addDocument(e.getValue(), e.getKey());
+    }
+    for (Map.Entry<String, org.torproject.onionoo.docs.SummaryDocument> e :
+        this.bridges.entrySet()) {
+      documentStore.addDocument(e.getValue(), e.getKey());
+    }
+    DocumentStoreFactory.setDocumentStore(documentStore);
+  }
+
+  private void createNodeIndexer() {
+    NodeIndexer newNodeIndexer = new NodeIndexer();
+    newNodeIndexer.startIndexing();
+    NodeIndexerFactory.setNodeIndexer(newNodeIndexer);
+  }
+
+  private void makeRequest(String request) throws IOException {
+    ResourceServlet rs = new ResourceServlet();
+    String requestParts[] = request.split("\\?");
+    String path = requestParts[0];
+    String queryString = requestParts.length > 1 ? requestParts[1] : null;
+    Map<String, String[]> parameterMap = parseParameters(request);
+    this.request = new TestingHttpServletRequestWrapper(path, queryString,
+        parameterMap);
+    this.response = new TestingHttpServletResponseWrapper();
+    rs.doGet(this.request, this.response);
+  }
+
+  private void parseResponse() {
+    this.responseString = this.response.getWrittenContent();
+    if (this.responseString != null) {
+      Gson gson = new Gson();
+      this.summaryDocument = gson.fromJson(this.responseString,
+          SummaryDocument.class);
+    }
+  }
+
+  private void assertErrorStatusCode(String request,
+      int errorStatusCode) {
+    this.runTest(request);
+    assertEquals(errorStatusCode, this.response.errorStatusCode);
+  }
+
+  private void assertSummaryDocument(String request,
+      int expectedRelaysNumber, String[] expectedRelaysNicknames,
+      int expectedBridgesNumber, String[] expectedBridgesNicknames) {
+    this.runTest(request);
+    assertNotNull(this.summaryDocument);
+    assertEquals(expectedRelaysNumber,
+        this.summaryDocument.relays.length);
+    if (expectedRelaysNicknames != null) {
+      for (int i = 0; i < expectedRelaysNumber; i++) {
+        assertEquals(expectedRelaysNicknames[i],
+            this.summaryDocument.relays[i].n);
+      }
+    }
+    assertEquals(expectedBridgesNumber,
+        this.summaryDocument.bridges.length);
+    if (expectedBridgesNicknames != null) {
+      for (int i = 0; i < expectedBridgesNumber; i++) {
+        assertEquals(expectedBridgesNicknames[i],
+            this.summaryDocument.bridges[i].n);
+      }
+    }
+  }
+
+  private Map<String, String[]> parseParameters(String request) {
+    Map<String, String[]> parameters = null;
+    String[] uriParts = request.split("\\?");
+    if (uriParts.length == 2) {
+      Map<String, List<String>> parameterLists =
+          new HashMap<String, List<String>>();
+      for (String parameter : uriParts[1].split("&")) {
+        String[] parameterParts = parameter.split("=");
+        if (!parameterLists.containsKey(parameterParts[0])) {
+          parameterLists.put(parameterParts[0],
+              new ArrayList<String>());
+        }
+        parameterLists.get(parameterParts[0]).add(parameterParts[1]);
+      }
+      parameters = new HashMap<String, String[]>();
+      for (Map.Entry<String, List<String>> e :
+          parameterLists.entrySet()) {
+        parameters.put(e.getKey(),
+            e.getValue().toArray(new String[e.getValue().size()]));
+      }
+    }
+    return parameters;
+  }
+
+  private static class SummaryDocument {
+    private String relays_published;
+    private RelaySummary[] relays;
+    private String bridges_published;
+    private BridgeSummary[] bridges;
+  }
+
+  private static class RelaySummary {
+    private String n;
+    private String f;
+    private String[] a;
+    private boolean r;
+  }
+
+  private static class BridgeSummary {
+    private String n;
+    private String h;
+    private boolean r;
+  }
+
+  @Test()
+  public void testValidSummaryRelay() throws IOException {
+    this.runTest("/summary");
+    assertEquals("2013-04-24 12:00:00",
+        this.summaryDocument.relays_published);
+    assertEquals(3, this.summaryDocument.relays.length);
+    RelaySummary relay = null;
+    for (RelaySummary r : this.summaryDocument.relays) {
+      if (r.f.equals("000C5F55BD4814B917CC474BD537F1A3B33CCE2A")) {
+        relay = r;
+        break;
+      }
+    }
+    assertNotNull(relay);
+    assertEquals("TorkaZ", relay.n);
+    assertEquals(3, relay.a.length);
+    assertEquals("62.216.201.221", relay.a[0]);
+    assertFalse(relay.r);
+  }
+
+  @Test()
+  public void testValidSummaryBridge() {
+    this.runTest("/summary");
+    assertEquals("2013-04-24 01:07:04",
+        this.summaryDocument.bridges_published);
+    assertEquals(3, this.summaryDocument.bridges.length);
+    BridgeSummary bridge = null;
+    for (BridgeSummary b : this.summaryDocument.bridges) {
+      if (b.h.equals("0000831B236DFF73D409AD17B40E2A728A53994F")) {
+        bridge = b;
+        break;
+      }
+    }
+    assertNotNull(bridge);
+    assertEquals("ec2bridgercc7f31fe", bridge.n);
+    assertFalse(bridge.r);
+  }
+
+  @Test()
+  public void testNonExistantDocumentType() {
+    this.assertErrorStatusCode(
+        "/doesnotexist", 400);
+  }
+
+  @Test()
+  public void testSUMMARYDocument() {
+    this.assertErrorStatusCode(
+        "/SUMMARY", 400);
+  }
+
+  @Test()
+  public void testTypeRelay() {
+    this.assertSummaryDocument(
+        "/summary?type=relay", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testTypeBridge() {
+    this.assertSummaryDocument(
+        "/summary?type=bridge", 0, null, 3, null);
+  }
+
+  @Test()
+  public void testTypeBridgerelay() {
+    this.assertErrorStatusCode(
+        "/summary?type=bridgerelay", 400);
+  }
+
+  @Test()
+  public void testTypeRelayBridge() {
+    this.assertSummaryDocument(
+        "/summary?type=relay&type=bridge", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testTypeBridgeRelay() {
+    this.assertSummaryDocument(
+        "/summary?type=bridge&type=relay", 0, null, 3, null);
+  }
+
+  @Test()
+  public void testTypeRelayRelay() {
+    this.assertSummaryDocument(
+        "/summary?type=relay&type=relay", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testTYPERelay() {
+    this.assertErrorStatusCode(
+        "/summary?TYPE=relay", 400);
+  }
+
+  @Test()
+  public void testTypeRELAY() {
+    this.assertSummaryDocument(
+        "/summary?type=RELAY", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testRunningTrue() {
+    this.assertSummaryDocument(
+        "/summary?running=true", 1, new String[] { "Ferrari458" }, 1,
+        new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testRunningFalse() {
+    this.assertSummaryDocument(
+        "/summary?running=false", 2, null, 2, null);
+  }
+
+  @Test()
+  public void testRunningTruefalse() {
+    this.assertErrorStatusCode(
+        "/summary?running=truefalse", 400);
+  }
+
+  @Test()
+  public void testRunningTrueFalse() {
+    this.assertSummaryDocument(
+        "/summary?running=true&running=false", 1,
+        new String[] { "Ferrari458" }, 1,  new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testRunningFalseTrue() {
+    this.assertSummaryDocument(
+        "/summary?running=false&running=true", 2, null, 2, null);
+  }
+
+  @Test()
+  public void testRunningTrueTrue() {
+    this.assertSummaryDocument(
+        "/summary?running=true&running=true", 1,
+        new String[] { "Ferrari458" }, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testRUNNINGTrue() {
+    this.assertErrorStatusCode(
+        "/summary?RUNNING=true", 400);
+  }
+
+  @Test()
+  public void testRunningTRUE() {
+    this.assertSummaryDocument(
+        "/summary?running=TRUE", 1, null, 1, null);
+  }
+
+  @Test()
+  public void testSearchTorkaZ() {
+    this.assertSummaryDocument(
+        "/summary?search=TorkaZ", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchTorkaX() {
+    this.assertSummaryDocument(
+        "/summary?search=TorkaX", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchOrkaZ() {
+    this.assertSummaryDocument(
+        "/summary?search=orkaZ", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchTorka() {
+    this.assertSummaryDocument(
+        "/summary?search=Torka", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchTORKAZ() {
+    this.assertSummaryDocument(
+        "/summary?search=TORKAZ", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=$000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarFingerprint39() {
+    this.assertSummaryDocument(
+        "/summary?search=$000C5F55BD4814B917CC474BD537F1A3B33CCE2", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarFingerprintLowerCase39() {
+    this.assertSummaryDocument(
+        "/summary?search=$000c5f55bd4814b917cc474bd537f1a3b33cce2", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchFingerprintLowerCase39() {
+    this.assertSummaryDocument(
+        "/summary?search=000c5f55bd4814b917cc474bd537f1a3b33cce2", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94cee", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarHashedFingerprint39() {
+    this.assertSummaryDocument(
+        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94ce", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchDollarHashedFingerprint41() {
+    this.assertErrorStatusCode(
+        "/summary?search=$5aa14c08d62913e0057a9ad5863b458c0ce94ceee",
+        400);
+  }
+
+  @Test()
+  public void testSearchBase64FingerprintAlphaNum() {
+    this.assertSummaryDocument(
+        "/summary?search=AAxfVb1IFLkXzEdL1Tfxo7M8zio", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64FingerprintSlash() {
+    this.assertSummaryDocument(
+        "/summary?search=ABwTs6Vacbl3ymXshVOdecZTo/w", 1,
+        new String[] { "Ferrari458" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64FingerprintPlus() {
+    this.assertSummaryDocument(
+        "/summary?search=ACXBNsHzqe7+KuP5GPA7+iG1Bws", 1,
+        new String[] { "TimMayTribute" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64FingerprintBridge() {
+    this.assertSummaryDocument(
+        "/summary?search=AACDGyNt/3PUCa0XtA4qcopTmU8", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64FingerprintPartial() {
+    this.assertSummaryDocument(
+        "/summary?search=AAx", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64HashedFingerprintTorkaZ() {
+    this.assertSummaryDocument(
+        "/summary?search=WqFMCNYpE+AFeprVhjtFjAzpTO4", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchBase64Fingerprint28() {
+    this.assertErrorStatusCode(
+        "/summary?search=AAAAAAAAAAAA//AAAAAAAAAAAAAA", 400);
+  }
+
+  @Test()
+  public void testSearchIp() {
+    this.assertSummaryDocument(
+        "/summary?search=62.216.201.221", 1, new String[] { "TorkaZ" }, 0,
+        null);
+  }
+
+  @Test()
+  public void testSearchIp24Network() {
+    this.assertSummaryDocument(
+        "/summary?search=62.216.201", 1, new String[] { "TorkaZ" }, 0,
+        null);
+  }
+
+  @Test()
+  public void testSearchIpExit() {
+    this.assertSummaryDocument(
+        "/summary?search=62.216.201.222", 1, new String[] { "TorkaZ" }, 0,
+        null);
+  }
+
+  @Test()
+  public void testSearchIpv6() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:4f8:3:2e::51]", 1,
+        new String[] { "Ferrari458" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6Slash64NoTrailingBracket() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:4f8:3:2e::", 1,
+        new String[] { "Ferrari458" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6Slash64TrailingBracket() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:4f8:3:2e::]", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6Uncompressed() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:04f8:0003:002e:0000:0000:0000:0051]", 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6UpperCase() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:4F8:3:2E::51]", 1,
+        new String[] { "Ferrari458" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6ThreeColons() {
+    this.assertSummaryDocument(
+        "/summary?search=[2001:4f8:3:2e:::51]", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6FiveHex() {
+    this.assertSummaryDocument(
+        "/summary?search=[20014:f80:3:2e::51]", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6NineGroups() {
+    this.assertSummaryDocument(
+        "/summary?search=[1:2:3:4:5:6:7:8:9]", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchIpv6TcpPort() {
+    this.assertErrorStatusCode(
+        "/summary?search=[2001:4f8:3:2e::51]:9001", 400);
+  }
+
+  @Test()
+  public void testSearchGummy() {
+    this.assertSummaryDocument(
+        "/summary?search=gummy", 0, null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchGummi() {
+    this.assertSummaryDocument(
+        "/summary?search=gummi", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchUmmy() {
+    this.assertSummaryDocument(
+        "/summary?search=ummy", 0, null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchGumm() {
+    this.assertSummaryDocument(
+        "/summary?search=gumm", 0, null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchGUMMY() {
+    this.assertSummaryDocument(
+        "/summary?search=GUMMY", 0, null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=$1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarHashedFingerprint39() {
+    this.assertSummaryDocument(
+        "/summary?search=$1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB75", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarHashedFingerprintLowerCase39() {
+    this.assertSummaryDocument(
+        "/summary?search=$1fede50ed8dba1dd9f9165f78c8131e4a44ab75", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeHashedFingerprintLowerCase39() {
+    this.assertSummaryDocument(
+        "/summary?search=1fede50ed8dba1dd9f9165f78c8131e4a44ab75", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarHashedHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=$CE52F898DB3678BCE33FAC28C92774DE90D618B5", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarHashedHashedFingerprint39() {
+    this.assertSummaryDocument(
+        "/summary?search=$CE52F898DB3678BCE33FAC28C92774DE90D618B", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testSearchBridgeDollarOriginalFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?search=$0010D49C6DA1E46A316563099F41BFE40B6C7183", 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testSearchUnderscore() {
+    this.assertErrorStatusCode(
+        "/summary?search=_", 400);
+  }
+
+  @Test()
+  public void testSearchTypeRelay() {
+    this.assertSummaryDocument("/summary?search=type:relay", 3, null, 0,
+        null);
+  }
+
+  @Test()
+  public void testSearchTypeRelayTorkaZ() {
+    this.assertSummaryDocument("/summary?search=type:relay TorkaZ", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchTorkaZTypeRelay() {
+    this.assertSummaryDocument("/summary?search=TorkaZ type:relay", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testSearchTypeRelayTypeDirectory() {
+    this.assertSummaryDocument(
+        "/summary?search=type:relay type:directory", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testSearchTypeDirectoryTypeRelay() {
+    this.assertErrorStatusCode(
+        "/summary?search=type:directory type:relay", 400);
+  }
+
+  @Test()
+  public void testSearchFooBar() {
+    this.assertErrorStatusCode("/summary?search=foo:bar", 400);
+  }
+
+  @Test()
+  public void testSearchSearchTorkaZ() {
+    this.assertErrorStatusCode("/summary?search=search:TorkaZ", 400);
+  }
+
+  @Test()
+  public void testSearchLimitOne() {
+    this.assertErrorStatusCode("/summary?search=limit:1", 400);
+  }
+
+  @Test()
+  public void testLookupFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testLookupDollarFingerprint() {
+    this.assertErrorStatusCode(
+        "/summary?lookup=$000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 400);
+  }
+
+  @Test()
+  public void testLookupDollarFingerprint39() {
+    this.assertErrorStatusCode(
+        "/summary?lookup=$000C5F55BD4814B917CC474BD537F1A3B33CCE2", 400);
+  }
+
+  @Test()
+  public void testLookupFingerprintLowerCase39() {
+    this.assertErrorStatusCode(
+        "/summary?lookup=000c5f55bd4814b917cc474bd537f1a3b33cce2", 400);
+  }
+
+  @Test()
+  public void testLookupHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=5aa14c08d62913e0057a9ad5863b458c0ce94cee", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testLookupBridgeHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testLookupBridgeHashedHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=CE52F898DB3678BCE33FAC28C92774DE90D618B5", 0,
+        null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testLookupBridgeOriginalFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=0010D49C6DA1E46A316563099F41BFE40B6C7183", 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testLookupNonExistantFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?lookup=0000000000000000000000000000000000000000", 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFingerprintRelayFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?fingerprint=000C5F55BD4814B917CC474BD537F1A3B33CCE2A",
+        1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testFingerprintRelayHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?fingerprint=5aa14c08d62913e0057a9ad5863b458c0ce94cee",
+        0, null, 0, null);
+  }
+
+  @Test()
+  public void testFingerprintBridgeHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?fingerprint=1FEDE50ED8DBA1DD9F9165F78C8131E4A44AB756",
+        0, null, 1, new String[] { "gummy" });
+  }
+
+  @Test()
+  public void testFingerprintBridgeHashedHashedFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?fingerprint=CE52F898DB3678BCE33FAC28C92774DE90D618B5",
+        0, null, 0, null);
+  }
+
+  @Test()
+  public void testFingerprintBridgeOriginalFingerprint() {
+    this.assertSummaryDocument(
+        "/summary?fingerprint=0010D49C6DA1E46A316563099F41BFE40B6C7183",
+        0, null, 0, null);
+  }
+
+  @Test()
+  public void testCountryDe() {
+    this.assertSummaryDocument(
+        "/summary?country=de", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testCountryFr() {
+    this.assertSummaryDocument(
+        "/summary?country=fr", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testCountryZz() {
+    this.assertSummaryDocument(
+        "/summary?country=zz", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testCountryDE() {
+    this.assertSummaryDocument(
+        "/summary?country=DE", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testCountryDeu() {
+    this.assertErrorStatusCode(
+        "/summary?country=deu", 400);
+  }
+
+  @Test()
+  public void testCountryD() {
+    this.assertErrorStatusCode(
+        "/summary?country=d", 400);
+  }
+
+  @Test()
+  public void testCountryA1() {
+    this.assertSummaryDocument(
+        "/summary?country=a1", 1, new String[] { "TimMayTribute" }, 0,
+        null);
+  }
+
+  @Test()
+  public void testCountryDeDe() {
+    this.assertSummaryDocument(
+        "/summary?country=de&country=de", 1, new String[] { "TorkaZ" }, 0,
+        null);
+  }
+
+  @Test()
+  public void testAsAS8767() {
+    this.assertSummaryDocument(
+        "/summary?as=AS8767", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testAs8767() {
+    this.assertSummaryDocument(
+        "/summary?as=8767", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testAsAS() {
+    this.assertErrorStatusCode(
+        "/summary?as=AS", 400);
+  }
+
+  @Test()
+  public void testAsas8767() {
+    this.assertSummaryDocument(
+        "/summary?as=as8767", 1, new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testAsASSpace8767() {
+    this.assertErrorStatusCode(
+        "/summary?as=AS 8767", 400);
+  }
+
+  @Test()
+  public void testFlagRunning() {
+    this.assertSummaryDocument(
+        "/summary?flag=Running", 3, null, 1, null);
+  }
+
+  @Test()
+  public void testFlagValid() {
+    this.assertSummaryDocument(
+        "/summary?flag=Valid", 3, null, 3, null);
+  }
+
+  @Test()
+  public void testFlagFast() {
+    this.assertSummaryDocument(
+        "/summary?flag=Fast", 2, null, 0, null);
+  }
+
+  @Test()
+  public void testFlagNamed() {
+    this.assertSummaryDocument(
+        "/summary?flag=Named", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testFlagUnnamed() {
+    this.assertSummaryDocument(
+        "/summary?flag=Unnamed", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testFlagV2Dir() {
+    this.assertSummaryDocument(
+        "/summary?flag=V2Dir", 2, null, 0, null);
+  }
+
+  @Test()
+  public void testFlagGuard() {
+    this.assertSummaryDocument(
+        "/summary?flag=Guard", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testFlagCool() {
+    this.assertSummaryDocument(
+        "/summary?flag=Cool", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysZeroToTwo() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=0-2", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysUpToThree() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=-3", 0, null, 1, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysThree() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=3", 0, null, 1, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysTwoToFive() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=2-5", 0, null, 1, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysSixToSixteen() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=6-16", 2, null, 1, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysNinetysevenOrMore() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=97-", 0, null, 1, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysNinetyeightOrMore() {
+    this.assertSummaryDocument(
+        "/summary?first_seen_days=98-", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysDashDash() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days=--", 400);
+  }
+
+  @Test()
+  public void testFirstSeenDaysDashOneDash() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days=-1-", 400);
+  }
+
+  @Test()
+  public void testFirstSeenDaysZeroDotDotOne() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days=0..1", 400);
+  }
+
+  @Test()
+  public void testFirstSeenDaysElevenDigits() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days=12345678901", 400);
+  }
+
+  @Test()
+  public void testFirstSeenDaysLargeTenDigitNumber() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days=9999999999", 400);
+  }
+
+  @Test()
+  public void testFirstSeenDaysMaxInt() {
+    this.assertSummaryDocument(
+        "/summary?last_seen_days=" + String.valueOf(Integer.MAX_VALUE), 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFirstSeenDaysMaxIntPlusOne() {
+    this.assertErrorStatusCode(
+        "/summary?first_seen_days="
+        + String.valueOf(Integer.MAX_VALUE + 1), 400);
+  }
+
+  @Test()
+  public void testLastSeenDaysZero() {
+    this.assertSummaryDocument(
+        "/summary?last_seen_days=0", 1, null, 1, null);
+  }
+
+  @Test()
+  public void testLastSeenDaysUpToZero() {
+    this.assertSummaryDocument(
+        "/summary?last_seen_days=-0", 1, null, 1, null);
+  }
+
+  @Test()
+  public void testLastSeenDaysOneToThree() {
+    this.assertSummaryDocument(
+        "/summary?last_seen_days=1-3", 1, null, 2, null);
+  }
+
+  @Test()
+  public void testLastSeenDaysSixOrMore() {
+    this.assertSummaryDocument(
+        "/summary?last_seen_days=6-", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testContactSteven() {
+    this.assertSummaryDocument(
+        "/summary?contact=Steven", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactStevenMurdoch() {
+    this.assertSummaryDocument(
+        "/summary?contact=Steven Murdoch", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactMurdochSteven() {
+    this.assertSummaryDocument(
+        "/summary?contact=Murdoch Steven", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactStevenDotMurdoch() {
+    this.assertSummaryDocument(
+        "/summary?contact=Steven.Murdoch", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactFbTokenFive() {
+    this.assertSummaryDocument(
+        "/summary?contact=<fb-token:5sR_K_zs2wM=>", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testContactFbToken() {
+    this.assertSummaryDocument(
+        "/summary?contact=<fb-token:", 2, null, 0, null);
+  }
+
+  @Test()
+  public void testContactDash() {
+    this.assertSummaryDocument(
+        "/summary?contact=-", 2, null, 0, null);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightAscending() {
+    this.assertSummaryDocument(
+        "/summary?order=consensus_weight", 3,
+        new String[] { "TorkaZ", "TimMayTribute", "Ferrari458" }, 3,
+        null);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightDescending() {
+    this.assertSummaryDocument(
+        "/summary?order=-consensus_weight", 3,
+        new String[] { "Ferrari458", "TimMayTribute", "TorkaZ" }, 3,
+        null);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightAscendingTwice() {
+    this.assertErrorStatusCode(
+        "/summary?order=consensus_weight,consensus_weight", 400);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightAscendingThenDescending() {
+    this.assertErrorStatusCode(
+        "/summary?order=consensus_weight,-consensus_weight", 400);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightThenNickname() {
+    this.assertErrorStatusCode(
+        "/summary?order=consensus_weight,nickname", 400);
+  }
+
+  @Test()
+  public void testOrderCONSENSUS_WEIGHT() {
+    this.assertSummaryDocument(
+        "/summary?order=CONSENSUS_WEIGHT", 3,
+        new String[] { "TorkaZ", "TimMayTribute", "Ferrari458" }, 3,
+        null);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightAscendingLimit1() {
+    this.assertSummaryDocument(
+        "/summary?order=consensus_weight&limit=1", 1,
+        new String[] { "TorkaZ" }, 0, null);
+  }
+
+  @Test()
+  public void testOrderConsensusWeightDecendingLimit1() {
+    this.assertSummaryDocument(
+        "/summary?order=-consensus_weight&limit=1", 1,
+        new String[] { "Ferrari458" }, 0, null);
+  }
+
+  @Test()
+  public void testOffsetOne() {
+    this.assertSummaryDocument(
+        "/summary?offset=1", 2, null, 3, null);
+  }
+
+  @Test()
+  public void testOffsetAllRelays() {
+    this.assertSummaryDocument(
+        "/summary?offset=3", 0, null, 3, null);
+  }
+
+  @Test()
+  public void testOffsetAllRelaysAndOneBridge() {
+    this.assertSummaryDocument(
+        "/summary?offset=4", 0, null, 2, null);
+  }
+
+  @Test()
+  public void testOffsetAllRelaysAndAllBridges() {
+    this.assertSummaryDocument(
+        "/summary?offset=6", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testOffsetMoreThanAllRelaysAndAllBridges() {
+    this.assertSummaryDocument(
+        "/summary?offset=7", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testOffsetZero() {
+    this.assertSummaryDocument(
+        "/summary?offset=0", 3, null, 3, null);
+  }
+
+  @Test()
+  public void testOffsetMinusOne() {
+    this.assertSummaryDocument(
+        "/summary?offset=-1", 3, null, 3, null);
+  }
+
+  @Test()
+  public void testOffsetOneWord() {
+    this.assertErrorStatusCode(
+        "/summary?offset=one", 400);
+  }
+
+  @Test()
+  public void testLimitOne() {
+    this.assertSummaryDocument(
+        "/summary?limit=1", 1, null, 0, null);
+  }
+
+  @Test()
+  public void testLimitAllRelays() {
+    this.assertSummaryDocument(
+        "/summary?limit=3", 3, null, 0, null);
+  }
+
+  @Test()
+  public void testLimitAllRelaysAndOneBridge() {
+    this.assertSummaryDocument(
+        "/summary?limit=4", 3, null, 1, null);
+  }
+
+  @Test()
+  public void testLimitAllRelaysAndAllBridges() {
+    this.assertSummaryDocument(
+        "/summary?limit=6", 3, null, 3, null);
+  }
+
+  @Test()
+  public void testLimitMoreThanAllRelaysAndAllBridges() {
+    this.assertSummaryDocument(
+        "/summary?limit=7", 3, null, 3, null);
+  }
+
+  @Test()
+  public void testLimitZero() {
+    this.assertSummaryDocument(
+        "/summary?limit=0", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testLimitMinusOne() {
+    this.assertSummaryDocument(
+        "/summary?limit=-1", 0, null, 0, null);
+  }
+
+  @Test()
+  public void testLimitOneWord() {
+    this.assertErrorStatusCode(
+        "/summary?limit=one", 400);
+  }
+
+  @Test()
+  public void testFamilyTorkaZ() {
+    this.assertSummaryDocument(
+        "/summary?family=000C5F55BD4814B917CC474BD537F1A3B33CCE2A", 2,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFamilyFerrari458() {
+    this.assertSummaryDocument(
+        "/summary?family=001C13B3A55A71B977CA65EC85539D79C653A3FC", 2,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFamilyTimMayTribute() {
+    this.assertSummaryDocument(
+        "/summary?family=0025C136C1F3A9EEFE2AE3F918F03BFA21B5070B", 1,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFamilyBridgegummy() {
+    this.assertSummaryDocument(
+        "/summary?family=0000831B236DFF73D409AD17B40E2A728A53994F", 0,
+        null, 0, null);
+  }
+
+  @Test()
+  public void testFamily39Characters() {
+    this.assertErrorStatusCode(
+        "/summary?family=00000000000000000000000000000000000000", 400);
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/DummyBridgeStatus.java b/src/test/java/org/torproject/onionoo/updater/DummyBridgeStatus.java
new file mode 100644
index 0000000..f6a396c
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/DummyBridgeStatus.java
@@ -0,0 +1,43 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.updater;
+
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.torproject.descriptor.BridgeNetworkStatus;
+import org.torproject.descriptor.NetworkStatusEntry;
+
+public class DummyBridgeStatus implements BridgeNetworkStatus {
+
+  public byte[] getRawDescriptorBytes() {
+    return null;
+  }
+
+  public List<String> getAnnotations() {
+    return null;
+  }
+
+  public List<String> getUnrecognizedLines() {
+    return null;
+  }
+
+  private long publishedMillis;
+  public void setPublishedMillis(long publishedMillis) {
+    this.publishedMillis = publishedMillis;
+  }
+  public long getPublishedMillis() {
+    return this.publishedMillis;
+  }
+
+  private SortedMap<String, NetworkStatusEntry> statusEntries =
+      new TreeMap<String, NetworkStatusEntry>();
+  public void addStatusEntry(NetworkStatusEntry statusEntry) {
+    this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
+  }
+  public SortedMap<String, NetworkStatusEntry> getStatusEntries() {
+    return this.statusEntries;
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/DummyConsensus.java b/src/test/java/org/torproject/onionoo/updater/DummyConsensus.java
new file mode 100644
index 0000000..8f164a0
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/DummyConsensus.java
@@ -0,0 +1,114 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.updater;
+
+import java.util.List;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+
+import org.torproject.descriptor.DirSourceEntry;
+import org.torproject.descriptor.DirectorySignature;
+import org.torproject.descriptor.NetworkStatusEntry;
+import org.torproject.descriptor.RelayNetworkStatusConsensus;
+
+public class DummyConsensus implements RelayNetworkStatusConsensus {
+
+  public byte[] getRawDescriptorBytes() {
+    return null;
+  }
+
+  public List<String> getAnnotations() {
+    return null;
+  }
+
+  public List<String> getUnrecognizedLines() {
+    return null;
+  }
+
+  public int getNetworkStatusVersion() {
+    return 0;
+  }
+
+  public String getConsensusFlavor() {
+    return null;
+  }
+
+  public int getConsensusMethod() {
+    return 0;
+  }
+
+  private long validAfterMillis;
+  public void setValidAfterMillis(long validAfterMillis) {
+    this.validAfterMillis = validAfterMillis;
+  }
+  public long getValidAfterMillis() {
+    return this.validAfterMillis;
+  }
+
+  public long getFreshUntilMillis() {
+    return 0;
+  }
+
+  public long getValidUntilMillis() {
+    return 0;
+  }
+
+  public long getVoteSeconds() {
+    return 0;
+  }
+
+  public long getDistSeconds() {
+    return 0;
+  }
+
+  public List<String> getRecommendedServerVersions() {
+    return null;
+  }
+
+  public List<String> getRecommendedClientVersions() {
+    return null;
+  }
+
+  public SortedSet<String> getKnownFlags() {
+    return null;
+  }
+
+  public SortedMap<String, Integer> getConsensusParams() {
+    return null;
+  }
+
+  public SortedMap<String, DirSourceEntry> getDirSourceEntries() {
+    return null;
+  }
+
+  private SortedMap<String, NetworkStatusEntry> statusEntries =
+      new TreeMap<String, NetworkStatusEntry>();
+  public void addStatusEntry(NetworkStatusEntry statusEntry) {
+    this.statusEntries.put(statusEntry.getFingerprint(), statusEntry);
+  }
+  public SortedMap<String, NetworkStatusEntry> getStatusEntries() {
+    return this.statusEntries;
+  }
+
+  public boolean containsStatusEntry(String fingerprint) {
+    return false;
+  }
+
+  public NetworkStatusEntry getStatusEntry(String fingerprint) {
+    return null;
+  }
+
+  public SortedMap<String, DirectorySignature> getDirectorySignatures() {
+    return null;
+  }
+
+  public SortedMap<String, Integer> getBandwidthWeights() {
+    return null;
+  }
+
+  public String getConsensusDigest() {
+    return null;
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/DummyDescriptorSource.java b/src/test/java/org/torproject/onionoo/updater/DummyDescriptorSource.java
new file mode 100644
index 0000000..3664e25
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/DummyDescriptorSource.java
@@ -0,0 +1,90 @@
+package org.torproject.onionoo.updater;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.torproject.descriptor.Descriptor;
+import org.torproject.onionoo.updater.DescriptorListener;
+import org.torproject.onionoo.updater.DescriptorSource;
+import org.torproject.onionoo.updater.DescriptorType;
+
+public class DummyDescriptorSource extends DescriptorSource {
+
+  private Map<DescriptorType, Set<Descriptor>> descriptors =
+      new HashMap<DescriptorType, Set<Descriptor>>();
+
+  public void provideDescriptors(DescriptorType descriptorType,
+      Collection<Descriptor> descriptors) {
+    for (Descriptor descriptor : descriptors) {
+      this.addDescriptor(descriptorType, descriptor);
+    }
+  }
+
+  public void addDescriptor(DescriptorType descriptorType,
+      Descriptor descriptor) {
+    this.getDescriptorsByType(descriptorType).add(descriptor);
+  }
+
+  private Set<Descriptor> getDescriptorsByType(
+      DescriptorType descriptorType) {
+    if (!this.descriptors.containsKey(descriptorType)) {
+      this.descriptors.put(descriptorType, new HashSet<Descriptor>());
+    }
+    return this.descriptors.get(descriptorType);
+  }
+
+  private Map<DescriptorType, Set<DescriptorListener>>
+      descriptorListeners = new HashMap<DescriptorType,
+      Set<DescriptorListener>>();
+
+  public void registerDescriptorListener(DescriptorListener listener,
+      DescriptorType descriptorType) {
+    if (!this.descriptorListeners.containsKey(descriptorType)) {
+      this.descriptorListeners.put(descriptorType,
+          new HashSet<DescriptorListener>());
+    }
+    this.descriptorListeners.get(descriptorType).add(listener);
+  }
+
+  public void readDescriptors() {
+    Set<DescriptorType> descriptorTypes = new HashSet<DescriptorType>();
+    descriptorTypes.addAll(this.descriptorListeners.keySet());
+    for (DescriptorType descriptorType : descriptorTypes) {
+      boolean relay;
+      switch (descriptorType) {
+      case RELAY_CONSENSUSES:
+      case RELAY_SERVER_DESCRIPTORS:
+      case RELAY_EXTRA_INFOS:
+      case EXIT_LISTS:
+        relay = true;
+        break;
+      case BRIDGE_STATUSES:
+      case BRIDGE_SERVER_DESCRIPTORS:
+      case BRIDGE_EXTRA_INFOS:
+      case BRIDGE_POOL_ASSIGNMENTS:
+      default:
+        relay = false;
+        break;
+      }
+      if (this.descriptors.containsKey(descriptorType) &&
+          this.descriptorListeners.containsKey(descriptorType)) {
+        Set<DescriptorListener> listeners =
+            this.descriptorListeners.get(descriptorType);
+        for (Descriptor descriptor :
+            this.getDescriptorsByType(descriptorType)) {
+          for (DescriptorListener listener : listeners) {
+            listener.processDescriptor(descriptor, relay);
+          }
+        }
+      }
+    }
+  }
+
+  public void writeHistoryFiles() {
+    /* Nothing to do here. */
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/DummyStatusEntry.java b/src/test/java/org/torproject/onionoo/updater/DummyStatusEntry.java
new file mode 100644
index 0000000..64ef73f
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/DummyStatusEntry.java
@@ -0,0 +1,92 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.updater;
+
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.torproject.descriptor.NetworkStatusEntry;
+
+public class DummyStatusEntry implements NetworkStatusEntry {
+
+  public DummyStatusEntry(String fingerprint) {
+    this.fingerprint = fingerprint;
+  }
+
+  public byte[] getStatusEntryBytes() {
+    return null;
+  }
+
+  @Override
+  public String getNickname() {
+    return null;
+  }
+
+  private String fingerprint;
+  public String getFingerprint() {
+    return this.fingerprint;
+  }
+
+  public String getDescriptor() {
+    return null;
+  }
+
+  public long getPublishedMillis() {
+    return 0;
+  }
+
+  public String getAddress() {
+    return null;
+  }
+
+  public int getOrPort() {
+    return 0;
+  }
+
+  public int getDirPort() {
+    return 0;
+  }
+
+  public Set<String> getMicrodescriptorDigests() {
+    return null;
+  }
+
+  public List<String> getOrAddresses() {
+    return null;
+  }
+
+  private SortedSet<String> flags = new TreeSet<String>();
+  public void addFlag(String flag) {
+    this.flags.add(flag);
+  }
+  public SortedSet<String> getFlags() {
+    return this.flags;
+  }
+
+  public String getVersion() {
+    return null;
+  }
+
+  public long getBandwidth() {
+    return 0;
+  }
+
+  public long getMeasured() {
+    return 0;
+  }
+
+  public boolean getUnmeasured() {
+    return false;
+  }
+
+  public String getDefaultPolicy() {
+    return null;
+  }
+
+  public String getPortList() {
+    return null;
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/LookupServiceTest.java b/src/test/java/org/torproject/onionoo/updater/LookupServiceTest.java
new file mode 100644
index 0000000..9275fbf
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/LookupServiceTest.java
@@ -0,0 +1,596 @@
+/* Copyright 2013 The Tor Project
+ * See LICENSE for licensing information */
+
+package org.torproject.onionoo.updater;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.torproject.onionoo.updater.LookupResult;
+import org.torproject.onionoo.updater.LookupService;
+
+public class LookupServiceTest {
+
+  private List<String> geoLite2CityBlocksIPv4Lines,
+      geoLite2CityLocationsEnLines, geoipASNum2Lines;
+
+  private LookupService lookupService;
+
+  private SortedSet<String> addressStrings = new TreeSet<String>();
+
+  private SortedMap<String, LookupResult> lookupResults;
+
+  private void populateLines() {
+    this.geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    this.geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    this.geoLite2CityBlocksIPv4Lines.add("8.8.0.0/21,6252001,6252001,,0,"
+        + "0,,38.0000,-97.0000");
+    this.geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,5375480,6252001,,0,"
+        + "0,94035,37.3860,-122.0838");
+    this.geoLite2CityBlocksIPv4Lines.add("8.8.9.0/24,6252001,6252001,,0,"
+        + "0,,38.0000,-97.0000");
+    this.geoLite2CityLocationsEnLines = new ArrayList<String>();
+    this.geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    this.geoLite2CityLocationsEnLines.add("6252001,en,NA,"
+        + "\"North America\",US,\"United States\",,,,,,,");
+    this.geoLite2CityLocationsEnLines.add("5375480,en,NA,"
+        + "\"North America\",US,\"United States\",CA,California,,,"
+        + "\"Mountain View\",807,America/Los_Angeles");
+    this.geoipASNum2Lines = new ArrayList<String>();
+    this.geoipASNum2Lines.add("134743296,134744063,\"AS3356 Level 3 "
+        + "Communications\"");
+    this.geoipASNum2Lines.add("134744064,134744319,\"AS15169 Google "
+        + "Inc.\"");
+    this.geoipASNum2Lines.add("134744320,134750463,\"AS3356 Level 3 "
+        + "Communications\"");
+  }
+
+  private void writeCsvFiles() {
+    try {
+      this.writeCsvFile(this.geoLite2CityBlocksIPv4Lines,
+          "GeoLite2-City-Blocks-IPv4.csv");
+      this.writeCsvFile(this.geoLite2CityLocationsEnLines,
+          "GeoLite2-City-Locations-en.csv");
+      this.writeCsvFile(this.geoipASNum2Lines, "GeoIPASNum2.csv");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private void writeCsvFile(List<String> lines, String fileName)
+      throws IOException {
+    if (lines != null && !lines.isEmpty()) {
+      BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(
+          new FileOutputStream(new File(this.tempGeoipDir, fileName)),
+          "UTF-8"));
+      for (String line : lines) {
+        bw.write(line + "\n");
+      }
+      bw.close();
+    }
+  }
+
+  private void performLookups() {
+    this.lookupService = new LookupService(this.tempGeoipDir);
+    this.lookupResults = this.lookupService.lookup(this.addressStrings);
+  }
+
+  private void assertLookupResult(List<String> geoLite2CityBlocksLines,
+      List<String> geoLite2CityLocationsLines,
+      List<String> geoipASNum2Lines, String addressString,
+      String countryCode, String countryName, String regionName,
+      String cityName, Float latitude, Float longitude, String aSNumber,
+      String aSName) {
+    this.addressStrings.add(addressString);
+    this.populateLines();
+    if (geoLite2CityBlocksLines != null) {
+      this.geoLite2CityBlocksIPv4Lines = geoLite2CityBlocksLines;
+    }
+    if (geoLite2CityLocationsLines != null) {
+      this.geoLite2CityLocationsEnLines = geoLite2CityLocationsLines;
+    }
+    if (geoipASNum2Lines != null) {
+      this.geoipASNum2Lines = geoipASNum2Lines;
+    }
+    this.writeCsvFiles();
+    /* Disable log messages printed to System.err. */
+    System.setErr(new PrintStream(new OutputStream() {
+      public void write(int b) {
+      }
+    }));
+    this.performLookups();
+    if (countryCode == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getCountryCode() == null);
+    } else {
+      assertEquals(countryCode,
+          this.lookupResults.get(addressString).getCountryCode());
+    }
+    if (countryName == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getCountryName() == null);
+    } else {
+      assertEquals(countryName,
+          this.lookupResults.get(addressString).getCountryName());
+    }
+    if (regionName == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getRegionName() == null);
+    } else {
+      assertEquals(regionName,
+          this.lookupResults.get(addressString).getRegionName());
+    }
+    if (cityName == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getCityName() == null);
+    } else {
+      assertEquals(cityName,
+          this.lookupResults.get(addressString).getCityName());
+    }
+    if (latitude == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getLatitude() == null);
+    } else {
+      assertEquals(latitude,
+          this.lookupResults.get(addressString).getLatitude(), 0.01);
+    }
+    if (longitude == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getLongitude() == null);
+    } else {
+      assertEquals(longitude,
+          this.lookupResults.get(addressString).getLongitude(), 0.01);
+    }
+    if (aSNumber == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getAsNumber() == null);
+    } else {
+      assertEquals(aSNumber,
+          this.lookupResults.get(addressString).getAsNumber());
+    }
+    if (aSName == null) {
+      assertTrue(!this.lookupResults.containsKey(addressString) ||
+          this.lookupResults.get(addressString).getAsName() == null);
+    } else {
+      assertEquals(aSName,
+          this.lookupResults.get(addressString).getAsName());
+    }
+  }
+
+  @Rule
+  public TemporaryFolder tempFolder = new TemporaryFolder();
+
+  private File tempGeoipDir;
+
+  @Before
+  public void createTempGeoipDir() throws IOException {
+    this.tempGeoipDir = this.tempFolder.newFolder("geoip");
+  }
+
+  @Test()
+  public void testLookup8888() {
+    this.assertLookupResult(null, null, null, "8.8.8.8", "us",
+        "United States", "California", "Mountain View", 37.3860f,
+        -122.0838f, "AS15169", "Google Inc.");
+  }
+
+  @Test()
+  public void testLookup8880() {
+    this.assertLookupResult(null, null, null, "8.8.8.0", "us",
+        "United States", "California", "Mountain View", 37.3860f,
+        -122.0838f, "AS15169", "Google Inc.");
+  }
+
+  @Test()
+  public void testLookup888255() {
+    this.assertLookupResult(null, null, null, "8.8.8.255", "us",
+        "United States", "California", "Mountain View", 37.3860f,
+        -122.0838f, "AS15169", "Google Inc.");
+  }
+
+  @Test()
+  public void testLookup888256() {
+    this.assertLookupResult(null, null, null, "8.8.8.256", null, null,
+        null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookup888Minus1() {
+    this.assertLookupResult(null, null, null, "8.8.8.-1", null, null,
+        null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookup000() {
+    this.assertLookupResult(null, null, null, "0.0.0.0", null, null, null,
+        null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupNoBlocksLines() {
+    this.assertLookupResult(new ArrayList<String>(), null, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupNoLocationLines() {
+    this.assertLookupResult(null, new ArrayList<String>(), null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupNoGeoipASNum2Lines() {
+    this.assertLookupResult(null, null, new ArrayList<String>(),
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupNoCorrespondingLocation() {
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("6252001,en,NA,"
+        + "\"North America\",US,\"United States\",,,,,,,");
+    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
+        "8.8.8.8", null, null, null, null, 37.3860f, -122.0838f,
+        "AS15169", "Google Inc.");
+  }
+
+  @Test()
+  public void testLookupBlocksStartNotANumber() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("one/24,5375480,6252001,,0,"
+        + "0,94035,37.3860,-122.0838");
+    this.assertLookupResult(
+        geoLite2CityBlocksIPv4Lines, null, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupBlocksLocationX() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,X,X,,0,0,94035,37.3860,"
+        + "-122.0838");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupBlocksLocationEmpty() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,,,,0,0,,,");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
+        "8.8.8.8", null, null, null, null, null, null, "AS15169",
+        "Google Inc.");
+  }
+
+  @Test()
+  public void testLookupBlocksTooFewFields() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("8.8.8.0/24,5375480,6252001,,0,"
+        + "0,94035,37.3860");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines, null, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationLocIdNotANumber() {
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("threetwoonenineone,en,NA,"
+        + "\"North America\",US,\"United States\",CA,California,,,"
+        + "\"Mountain View\",807,America/Los_Angeles");
+    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationTooFewFields() {
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("threetwoonenineone,en,NA,"
+        + "\"North America\",US,\"United States\",CA,California,,,"
+        + "\"Mountain View\",807");
+    this.assertLookupResult(null, geoLite2CityLocationsEnLines, null,
+        "8.8.8.8", null, null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupGeoipASNum2EndBeforeStart() {
+    List<String> geoipASNum2Lines = new ArrayList<String>();
+    geoipASNum2Lines.add("134743296,134744063,\"AS3356 Level 3 "
+        + "Communications\"");
+    geoipASNum2Lines.add("134744319,134744064,\"AS15169 Google Inc.\"");
+    geoipASNum2Lines.add("134744320,134750463,\"AS3356 Level 3 "
+        + "Communications\"");
+    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", "us",
+        "United States", "California", "Mountain View", 37.3860f,
+        -122.0838f, null, null);
+  }
+
+  @Test()
+  public void testLookupGeoipASNum2StartNotANumber() {
+    List<String> geoipASNum2Lines = new ArrayList<String>();
+    geoipASNum2Lines.add("one,134744319,\"AS15169 Google Inc.\"");
+    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
+        null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupGeoipASNum2StartTooLarge() {
+    List<String> geoipASNum2Lines = new ArrayList<String>();
+    geoipASNum2Lines.add("1" + String.valueOf(Long.MAX_VALUE)
+        + ",134744319,\"AS15169 Google Inc.\"");
+    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
+        null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupGeoipASNum2TooFewFields() {
+    List<String> geoipASNum2Lines = new ArrayList<String>();
+    geoipASNum2Lines.add("134744064,134744319");
+    this.assertLookupResult(null, null, geoipASNum2Lines, "8.8.8.8", null,
+        null, null, null, null, null, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationTurkey() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("46.1.133.0/24,307515,298795,,0,0,,"
+        + "39.1458,34.1639");
+    geoLite2CityBlocksIPv4Lines.add("46.196.12.0/24,738927,298795,,0,0,,"
+        + "40.9780,27.5085");
+    geoLite2CityBlocksIPv4Lines.add("78.180.14.0/24,745169,298795,,0,0,,"
+        + "40.0781,29.5133");
+    geoLite2CityBlocksIPv4Lines.add("81.215.1.0/24,749748,298795,,0,0,,"
+        + "40.6000,33.6153");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("307515,en,AS,Asia,TR,Turkey,40,"
+        + "\"K\u0131r\u015Fehir\",,,\"K\u0131r\u015Fehir\",,"
+        + "Europe/Istanbul");
+    geoLite2CityLocationsEnLines.add("738927,en,AS,Asia,TR,Turkey,59,"
+        + "\"Tekirda\u011F\",,,\"Tekirda\u011F\",,Europe/Istanbul");
+    geoLite2CityLocationsEnLines.add("745169,en,AS,Asia,TR,Turkey,16,"
+        + "Bursa,,,\u0130neg\u00F6l,,Europe/Istanbul");
+    geoLite2CityLocationsEnLines.add("749748,en,AS,Asia,TR,Turkey,18,"
+        + "\"\u00C7ank\u0131r\u0131\",,,\"\u00C7ank\u0131r\u0131\",,"
+        + "Europe/Istanbul");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "46.1.133.0", "tr", "Turkey",
+        "K\u0131r\u015Fehir", "K\u0131r\u015Fehir", 39.1458f, 34.1639f,
+        null, null);
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "46.196.12.0", "tr", "Turkey",
+        "Tekirda\u011F", "Tekirda\u011F", 40.9780f, 27.5085f, null, null);
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "78.180.14.0", "tr", "Turkey",
+        "Bursa", "\u0130neg\u00F6l", 40.0781f, 29.5133f, null, null);
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "81.215.1.0", "tr", "Turkey",
+        "\u00C7ank\u0131r\u0131", "\u00C7ank\u0131r\u0131", 40.6000f,
+        33.6153f, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationLatvia() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("78.28.192.0/24,456202,458258,,0,0,,"
+        + "56.5000,27.3167");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("456202,en,EU,Europe,LV,Latvia,REZ,"
+        + "Rezekne,,,\"R\u0113zekne\",,Europe/Riga");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "78.28.192.0", "lv", "Latvia",
+        "Rezekne", "R\u0113zekne", 56.5000f, 27.3167f, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationAzerbaijan() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("94.20.148.0/24,585170,587116,,0,0,,"
+        + "41.1919,47.1706");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("585170,en,AS,Asia,AZ,Azerbaijan,"
+        + "SAK,\"Shaki City\",,,\"\u015E\u01DDki\",,Asia/Baku");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "94.20.148.0", "az",
+        "Azerbaijan", "Shaki City", "\u015E\u01DDki", 41.1919f, 47.1706f,
+        null, null);
+  }
+
+  @Test()
+  public void testLookupLocationVietnam() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("115.78.92.0/23,1587976,1562822,,0,0,"
+        + ",10.2333,106.3833");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("1587976,en,AS,Asia,VN,Vietnam,50,"
+        + "\"Tinh Ben Tre\",,,\"B\u1EBFn Tre\",,Asia/Ho_Chi_Minh");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "115.78.92.0", "vn",
+        "Vietnam", "Tinh Ben Tre", "B\u1EBFn Tre", 10.2333f, 106.3833f,
+        null, null);
+  }
+
+  @Test()
+  public void testLookupLocationJapan() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("113.154.131.0/24,1848333,1861060,,0,"
+        + "0,1012236,35.8000,139.1833");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("1848333,en,AS,Asia,JP,Japan,13,"
+        + "\"T\u014Dky\u014D\",,,Yokoo,,Asia/Tokyo");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "113.154.131.0", "jp",
+        "Japan", "T\u014Dky\u014D", "Yokoo", 35.8000f, 139.1833f, null,
+        null);
+  }
+
+  @Test()
+  public void testLookupLocationDenmark() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("2.110.246.0/24,2625001,2623032,,0,0,"
+        + "5970,54.8880,10.4112");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("2625001,en,EU,Europe,DK,Denmark,83,"
+        + "\"South Denmark\",,,\"\u00C6r\u00F8sk\u00F8bing\",,"
+        + "Europe/Copenhagen");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "2.110.246.0", "dk",
+        "Denmark", "South Denmark", "\u00C6r\u00F8sk\u00F8bing", 54.8880f,
+        10.4112f, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationGermany() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("37.209.30.128/25,2947444,2921044,,0,"
+        + "0,,48.6833,9.0167");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("2947444,en,EU,Europe,DE,Germany,BW,"
+        + "\"Baden-W\u00FCrttemberg Region\",,,B\u00F6blingen,,"
+        + "Europe/Berlin");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "37.209.30.128", "de",
+        "Germany", "Baden-W\u00FCrttemberg Region", "B\u00F6blingen",
+        48.6833f, 9.0167f, null, null);
+  }
+
+  @Test()
+  public void testLookupLocationPoland() {
+    List<String> geoLite2CityBlocksIPv4Lines = new ArrayList<String>();
+    geoLite2CityBlocksIPv4Lines.add("network,geoname_id,"
+        + "registered_country_geoname_id,represented_country_geoname_id,"
+        + "is_anonymous_proxy,is_satellite_provider,postal_code,latitude,"
+        + "longitude");
+    geoLite2CityBlocksIPv4Lines.add("5.185.94.0/24,3099434,798544,,0,0,,"
+        + "54.3608,18.6583");
+    List<String> geoLite2CityLocationsEnLines = new ArrayList<String>();
+    geoLite2CityLocationsEnLines.add("geoname_id,locale_code,"
+        + "continent_code,continent_name,country_iso_code,country_name,"
+        + "subdivision_1_iso_code,subdivision_1_name,"
+        + "subdivision_2_iso_code,subdivision_2_name,city_name,"
+        + "metro_code,time_zone");
+    geoLite2CityLocationsEnLines.add("3099434,en,EU,Europe,PL,Poland,PM,"
+        + "\"Pomeranian Voivodeship\",,,\"Gda\u0144sk\",,Europe/Warsaw");
+    this.assertLookupResult(geoLite2CityBlocksIPv4Lines,
+        geoLite2CityLocationsEnLines, null, "5.185.94.0", "pl", "Poland",
+        "Pomeranian Voivodeship", "Gda\u0144sk", 54.3608f, 18.6583f, null,
+        null);
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/updater/UptimeStatusUpdaterTest.java b/src/test/java/org/torproject/onionoo/updater/UptimeStatusUpdaterTest.java
new file mode 100644
index 0000000..e74dc5c
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/updater/UptimeStatusUpdaterTest.java
@@ -0,0 +1,182 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.updater;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.torproject.onionoo.docs.DateTimeHelper;
+import org.torproject.onionoo.docs.DocumentStoreFactory;
+import org.torproject.onionoo.docs.DummyDocumentStore;
+import org.torproject.onionoo.docs.UptimeHistory;
+import org.torproject.onionoo.docs.UptimeStatus;
+
+public class UptimeStatusUpdaterTest {
+
+  private DummyDescriptorSource descriptorSource;
+
+  @Before
+  public void createDummyDescriptorSource() {
+    this.descriptorSource = new DummyDescriptorSource();
+    DescriptorSourceFactory.setDescriptorSource(this.descriptorSource);
+  }
+
+  private DummyDocumentStore documentStore;
+
+  @Before
+  public void createDummyDocumentStore() {
+    this.documentStore = new DummyDocumentStore();
+    DocumentStoreFactory.setDocumentStore(this.documentStore);
+  }
+
+  @Test
+  public void testNoDescriptorsNoStatusFiles() {
+    UptimeStatusUpdater updater = new UptimeStatusUpdater();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    updater.updateStatuses();
+    assertEquals("Without providing any data, nothing should be written "
+        + "to disk.", 0,
+        this.documentStore.getPerformedStoreOperations());
+  }
+
+  private static final long VALID_AFTER_SAMPLE =
+      DateTimeHelper.parse("2014-03-21 20:00:00");
+
+  private static final String GABELMOO_FINGERPRINT =
+      "F2044413DAC2E02E3D6BCF4735A19BCA1DE97281";
+
+  private void addConsensusSample() {
+    DummyStatusEntry statusEntry = new DummyStatusEntry(
+        GABELMOO_FINGERPRINT);
+    statusEntry.addFlag("Running");
+    DummyConsensus consensus = new DummyConsensus();
+    consensus.setValidAfterMillis(VALID_AFTER_SAMPLE);
+    consensus.addStatusEntry(statusEntry);
+    this.descriptorSource.addDescriptor(DescriptorType.RELAY_CONSENSUSES,
+        consensus);
+  }
+
+  @Test
+  public void testOneConsensusNoStatusFiles() {
+    this.addConsensusSample();
+    UptimeStatusUpdater updater = new UptimeStatusUpdater();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    updater.updateStatuses();
+    assertEquals("Two status files should have been written to disk.",
+        2, this.documentStore.getPerformedStoreOperations());
+    for (String fingerprint : new String[] { GABELMOO_FINGERPRINT,
+        null }) {
+      UptimeStatus status = this.documentStore.getDocument(
+          UptimeStatus.class, fingerprint);
+      UptimeHistory history = status.getRelayHistory().first();
+      assertEquals("History must contain one entry.", 1,
+          status.getRelayHistory().size());
+      assertEquals("History not for relay.", true, history.isRelay());
+      assertEquals("History start millis not as expected.",
+          VALID_AFTER_SAMPLE, history.getStartMillis());
+      assertEquals("History uptime hours must be 1.", 1,
+          history.getUptimeHours());
+    }
+  }
+
+  private static final String ALL_RELAYS_AND_BRIDGES_FINGERPRINT = null;
+
+  private static final String ALL_RELAYS_AND_BRIDGES_UPTIME_SAMPLE =
+      "r 2013-07-22-17 5811\n" /* ends 2014-03-21 20:00:00 */
+      + "b 2013-07-22-17 5811\n"; /* ends 2014-03-21 20:00:00 */
+
+  private void addAllRelaysAndBridgesUptimeSample() {
+    UptimeStatus uptimeStatus = new UptimeStatus();
+    uptimeStatus.setFromDocumentString(
+        ALL_RELAYS_AND_BRIDGES_UPTIME_SAMPLE);
+    this.documentStore.addDocument(uptimeStatus,
+        ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
+  }
+
+  @Test
+  public void testOneConsensusOneStatusFiles() {
+    this.addAllRelaysAndBridgesUptimeSample();
+    this.addConsensusSample();
+    UptimeStatusUpdater updater = new UptimeStatusUpdater();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    updater.updateStatuses();
+    assertEquals("Two status files should have been written to disk.",
+        2, this.documentStore.getPerformedStoreOperations());
+    UptimeStatus status = this.documentStore.getDocument(
+        UptimeStatus.class, ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
+    assertEquals("Relay history must contain one entry", 1,
+        status.getRelayHistory().size());
+    UptimeHistory history = status.getRelayHistory().first();
+    assertEquals("History not for relay.", true, history.isRelay());
+    assertEquals("History start millis not as expected.",
+        DateTimeHelper.parse("2013-07-22 17:00:00"),
+        history.getStartMillis());
+    assertEquals("History uptime hours must be 5812.", 5812,
+        history.getUptimeHours());
+  }
+
+  private static final long PUBLISHED_SAMPLE =
+      DateTimeHelper.parse("2014-03-21 20:37:03");
+
+  private static final String NDNOP2_FINGERPRINT =
+      "DE6397A047ABE5F78B4C87AF725047831B221AAB";
+
+  private void addBridgeStatusSample() {
+    DummyStatusEntry statusEntry = new DummyStatusEntry(
+        NDNOP2_FINGERPRINT);
+    statusEntry.addFlag("Running");
+    DummyBridgeStatus bridgeStatus = new DummyBridgeStatus();
+    bridgeStatus.setPublishedMillis(PUBLISHED_SAMPLE);
+    bridgeStatus.addStatusEntry(statusEntry);
+    this.descriptorSource.addDescriptor(DescriptorType.BRIDGE_STATUSES,
+        bridgeStatus);
+  }
+
+  @Test
+  public void testOneBridgeStatusNoStatusFiles() {
+    this.addBridgeStatusSample();
+    UptimeStatusUpdater updater = new UptimeStatusUpdater();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    updater.updateStatuses();
+    assertEquals("Two status files should have been written to disk.",
+        2, this.documentStore.getPerformedStoreOperations());
+    for (String fingerprint : new String[] { NDNOP2_FINGERPRINT,
+        null }) {
+      UptimeStatus status = this.documentStore.getDocument(
+          UptimeStatus.class, fingerprint);
+      UptimeHistory history = status.getBridgeHistory().first();
+      assertEquals("Bridge history must contain one entry.", 1,
+          status.getBridgeHistory().size());
+      assertEquals("History not for bridge.", false, history.isRelay());
+      assertEquals("History start millis not as expected.",
+          DateTimeHelper.parse("2014-03-21 20:00:00"),
+          history.getStartMillis());
+      assertEquals("History uptime hours must be 1.", 1,
+          history.getUptimeHours());
+    }
+  }
+
+  @Test
+  public void testOneBridgeStatusOneStatusFiles() {
+    this.addAllRelaysAndBridgesUptimeSample();
+    this.addBridgeStatusSample();
+    UptimeStatusUpdater updater = new UptimeStatusUpdater();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    updater.updateStatuses();
+    assertEquals("Two status files should have been written to disk.",
+        2, this.documentStore.getPerformedStoreOperations());
+    UptimeStatus status = this.documentStore.getDocument(
+        UptimeStatus.class, ALL_RELAYS_AND_BRIDGES_FINGERPRINT);
+    assertEquals("Bridge history must contain one entry.", 1,
+        status.getBridgeHistory().size());
+    UptimeHistory history = status.getBridgeHistory().last();
+    assertEquals("History not for bridge.", false, history.isRelay());
+    assertEquals("History start millis not as expected.",
+        DateTimeHelper.parse("2013-07-22 17:00:00"),
+        history.getStartMillis());
+    assertEquals("History uptime hours must be 5812.", 5812,
+        history.getUptimeHours());
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/util/DummyTime.java b/src/test/java/org/torproject/onionoo/util/DummyTime.java
new file mode 100644
index 0000000..ec7a50f
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/util/DummyTime.java
@@ -0,0 +1,16 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.util;
+
+import org.torproject.onionoo.util.Time;
+
+public class DummyTime extends Time {
+  private long currentTimeMillis;
+  public DummyTime(long currentTimeMillis) {
+    this.currentTimeMillis = currentTimeMillis;
+  }
+  public long currentTimeMillis() {
+    return this.currentTimeMillis;
+  }
+}
+
diff --git a/src/test/java/org/torproject/onionoo/writer/UptimeDocumentWriterTest.java b/src/test/java/org/torproject/onionoo/writer/UptimeDocumentWriterTest.java
new file mode 100644
index 0000000..517efc0
--- /dev/null
+++ b/src/test/java/org/torproject/onionoo/writer/UptimeDocumentWriterTest.java
@@ -0,0 +1,262 @@
+/* Copyright 2014 The Tor Project
+ * See LICENSE for licensing information */
+package org.torproject.onionoo.writer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.torproject.onionoo.docs.DateTimeHelper;
+import org.torproject.onionoo.docs.DocumentStoreFactory;
+import org.torproject.onionoo.docs.DummyDocumentStore;
+import org.torproject.onionoo.docs.GraphHistory;
+import org.torproject.onionoo.docs.UptimeDocument;
+import org.torproject.onionoo.docs.UptimeStatus;
+import org.torproject.onionoo.updater.DescriptorSourceFactory;
+import org.torproject.onionoo.updater.DummyDescriptorSource;
+import org.torproject.onionoo.util.DummyTime;
+import org.torproject.onionoo.util.TimeFactory;
+
+public class UptimeDocumentWriterTest {
+
+  private static final long TEST_TIME = DateTimeHelper.parse(
+      "2014-03-23 12:00:00");
+
+  private DummyTime dummyTime;
+
+  @Before
+  public void createDummyTime() {
+    this.dummyTime = new DummyTime(TEST_TIME);
+    TimeFactory.setTime(this.dummyTime);
+  }
+
+  private DummyDescriptorSource descriptorSource;
+
+  @Before
+  public void createDummyDescriptorSource() {
+    this.descriptorSource = new DummyDescriptorSource();
+    DescriptorSourceFactory.setDescriptorSource(this.descriptorSource);
+  }
+
+  private DummyDocumentStore documentStore;
+
+  @Before
+  public void createDummyDocumentStore() {
+    this.documentStore = new DummyDocumentStore();
+    DocumentStoreFactory.setDocumentStore(this.documentStore);
+  }
+
+  @Test
+  public void testNoStatuses() {
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    writer.writeDocuments();
+    assertEquals("Without providing any data, nothing should be written "
+        + "to disk.", 0,
+        this.documentStore.getPerformedStoreOperations());
+  }
+
+  private static final String ALL_RELAYS_FINGERPRINT = null;
+
+  private static final String GABELMOO_FINGERPRINT =
+      "F2044413DAC2E02E3D6BCF4735A19BCA1DE97281";
+
+  private void addStatusOneWeekSample(String allRelaysUptime,
+      String gabelmooUptime) {
+    UptimeStatus status = new UptimeStatus();
+    status.setFromDocumentString(allRelaysUptime);
+    this.documentStore.addDocument(status, ALL_RELAYS_FINGERPRINT);
+    status = new UptimeStatus();
+    status.setFromDocumentString(gabelmooUptime);
+    this.documentStore.addDocument(status, GABELMOO_FINGERPRINT);
+  }
+
+  private static final long ONE_SECOND = 1000L,
+      ONE_HOUR = 60L * 60L * ONE_SECOND, FOUR_HOURS = 4L * ONE_HOUR;
+
+  private void assertOneWeekGraph(UptimeDocument document, int graphs,
+      String first, String last, int count, List<Integer> values) {
+    this.assertGraph(document, graphs, "1_week", first, last,
+        (int) (ONE_HOUR / ONE_SECOND), count, values);
+  }
+
+  private void assertOneMonthGraph(UptimeDocument document, int graphs,
+      String first, String last, int count, List<Integer> values) {
+    this.assertGraph(document, graphs, "1_month", first, last,
+        (int) (FOUR_HOURS / ONE_SECOND), count, values);
+  }
+
+  private void assertGraph(UptimeDocument document, int graphs,
+      String graphName, String first, String last, int interval,
+      int count, List<Integer> values) {
+    assertEquals("Should contain exactly " + graphs + " graphs.", graphs,
+        document.getUptime().size());
+    assertTrue("Should contain a graph for " + graphName + ".",
+        document.getUptime().containsKey(graphName));
+    GraphHistory history = document.getUptime().get(graphName);
+    assertEquals("First data point should be " + first + ".",
+        DateTimeHelper.parse(first), history.getFirst());
+    assertEquals("Last data point should be " + last + ".",
+        DateTimeHelper.parse(last), history.getLast());
+    assertEquals("Interval should be " + interval + " seconds.", interval,
+        (int) history.getInterval());
+    assertEquals("Factor should be 1.0 / 999.0.", 1.0 / 999.0,
+        (double) history.getFactor(), 0.01);
+    assertEquals("There should be one data point per hour.", count,
+        (int) history.getCount());
+    assertEquals("Count should be the same as the number of values.",
+        count, history.getValues().size());
+    if (values == null) {
+      for (int value : history.getValues()) {
+        assertEquals("All values should be 999.", 999, value);
+      }
+    } else {
+      assertEquals("Values are not as expected.", values,
+          history.getValues());
+    }
+  }
+
+  @Test
+  public void testOneHourUptime() {
+    this.addStatusOneWeekSample("r 2014-03-23-11 1\n",
+        "r 2014-03-23-11 1\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    assertEquals("Should not contain any graph.", 0,
+        document.getUptime().size());
+  }
+
+  @Test
+  public void testTwoHoursUptime() {
+    this.addStatusOneWeekSample("r 2014-03-23-10 2\n",
+        "r 2014-03-23-10 2\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneWeekGraph(document, 1, "2014-03-23 10:30:00",
+        "2014-03-23 11:30:00", 2, null);
+  }
+
+  @Test
+  public void testTwoHoursUptimeSeparatedByNull() {
+    this.addStatusOneWeekSample("r 2014-03-23-09 1\nr 2014-03-23-11 1\n",
+        "r 2014-03-23-09 1\nr 2014-03-23-11 1\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    assertEquals("Should not contain any graph.", 0,
+        document.getUptime().size());
+  }
+
+  @Test
+  public void testTwoHoursUptimeSeparatedByZero() {
+    this.addStatusOneWeekSample("r 2014-03-23-09 3\n",
+        "r 2014-03-23-09 1\nr 2014-03-23-11 1\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneWeekGraph(document, 1, "2014-03-23 09:30:00",
+        "2014-03-23 11:30:00", 3,
+        Arrays.asList(new Integer[] { 999, 0, 999 }));
+  }
+
+  @Test
+  public void testTwoHoursUptimeThenDowntime() {
+    this.addStatusOneWeekSample("r 2014-03-23-09 3\n",
+        "r 2014-03-23-09 2\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneWeekGraph(document, 1, "2014-03-23 09:30:00",
+        "2014-03-23 11:30:00", 3,
+        Arrays.asList(new Integer[] { 999, 999, 0 }));
+  }
+
+  @Test
+  public void testOneWeekUptime() {
+    this.addStatusOneWeekSample("r 2014-03-16-12 168\n",
+        "r 2014-03-16-12 168\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneWeekGraph(document, 1, "2014-03-16 12:30:00",
+        "2014-03-23 11:30:00", 168, null);
+  }
+
+  @Test
+  public void testOneWeekOneHourUptime() {
+    this.addStatusOneWeekSample("r 2014-03-16-11 169\n",
+        "r 2014-03-16-11 169\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneWeekGraph(document, 2, "2014-03-16 12:30:00",
+        "2014-03-23 11:30:00", 168, null);
+    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
+        "2014-03-23 10:00:00", 43, null);
+  }
+
+  @Test
+  public void testOneMonthPartialIntervalOnline() {
+    this.addStatusOneWeekSample("r 2014-03-16-08 8\n",
+        "r 2014-03-16-11 5\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
+        "2014-03-16 14:00:00", 2, null);
+  }
+
+  @Test
+  public void testOneMonthPartialIntervalOnOff() {
+    this.addStatusOneWeekSample("r 2014-03-16-08 8\n",
+        "r 2014-03-16-10 1\nr 2014-03-16-12 1\n");
+    UptimeDocumentWriter writer = new UptimeDocumentWriter();
+    DescriptorSourceFactory.getDescriptorSource().readDescriptors();
+    writer.writeDocuments();
+    assertEquals("Should write exactly one document.", 1,
+        this.documentStore.getPerformedStoreOperations());
+    UptimeDocument document = this.documentStore.getDocument(
+        UptimeDocument.class, GABELMOO_FINGERPRINT);
+    this.assertOneMonthGraph(document, 2, "2014-03-16 10:00:00",
+        "2014-03-16 14:00:00", 2,
+        Arrays.asList(new Integer[] { 499, 249 }));
+  }
+}
+



More information about the tor-commits mailing list