[tor-commits] [exonerator/master] Make ExoneraTor's code testable.

karsten at torproject.org karsten at torproject.org
Fri Sep 15 12:18:16 UTC 2017


commit ab2f5660c17887a84140dd5e50c4e24e6daedb2b
Author: iwakeh <iwakeh at torproject.org>
Date:   Tue Sep 5 13:21:27 2017 +0000

    Make ExoneraTor's code testable.
    
    This includes making utils methods static and also moving all
    Gson/JSON related code into QueryResponse for better encapsulation.
---
 .../torproject/exonerator/ExoneraTorServlet.java   | 89 +++++++++------------
 .../org/torproject/exonerator/QueryResponse.java   | 92 +++++++++++++++++++++-
 2 files changed, 129 insertions(+), 52 deletions(-)

diff --git a/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
index 749becc..8e88882 100644
--- a/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
+++ b/src/main/java/org/torproject/exonerator/ExoneraTorServlet.java
@@ -3,10 +3,10 @@
 
 package org.torproject.exonerator;
 
-import com.google.gson.Gson;
 import org.apache.commons.lang.StringEscapeUtils;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -40,12 +40,6 @@ public class ExoneraTorServlet extends HttpServlet {
 
   private String exoneraTorHost = "https://exonerator.torproject.org";
 
-  /* Don't accept query responses with versions lower than this. */
-  private static final String firstRecognizedVersion = "1.0";
-
-  /* Don't accept query responses with this version or higher. */
-  private static final String firstUnrecognizedVersion = "2.0";
-
   private List<String> availableLanguages =
       Arrays.asList("de", "en", "fr", "ro", "sv");
 
@@ -54,10 +48,7 @@ public class ExoneraTorServlet extends HttpServlet {
   @Override
   public void init(ServletConfig config) throws ServletException {
     super.init(config);
-
-    /* Initialize logger. */
     this.logger = Logger.getLogger(ExoneraTorServlet.class.toString());
-
     this.availableLanguageNames = new TreeMap<>();
     for (String locale : this.availableLanguages) {
       ResourceBundle rb = ResourceBundle.getBundle("ExoneraTor",
@@ -76,13 +67,12 @@ public class ExoneraTorServlet extends HttpServlet {
 
     /* Parse ip parameter. */
     String ipParameter = request.getParameter("ip");
-    String relayIp = this.parseIpParameter(ipParameter);
+    String relayIp = parseIpParameter(ipParameter);
     final boolean relayIpHasError = relayIp == null;
 
     /* Parse timestamp parameter. */
     String timestampParameter = request.getParameter("timestamp");
-    String timestampStr = this.parseTimestampParameter(
-        timestampParameter);
+    String timestampStr = parseTimestampParameter(timestampParameter);
     final boolean timestampHasError = timestampStr == null;
 
     /* Parse lang parameter. */
@@ -93,21 +83,21 @@ public class ExoneraTorServlet extends HttpServlet {
       langStr = langParameter;
     }
 
-    /* Step 2: Query the database. */
+    /* Step 2: Query the backend server. */
 
-    boolean successfullyConnectedToDatabase = false;
+    boolean successfullyConnectedToBackend = false;
     String firstDate = null;
     String lastDate = null;
     boolean noRelevantConsensuses = true;
     List<String[]> statusEntries = new ArrayList<>();
     List<String> addressesInSameNetwork = null;
 
-    /* Only query the database if we received valid user input. */
+    /* Only query, if we received valid user input. */
     if (null != relayIp && !relayIp.isEmpty() && null != timestampStr
         && !timestampStr.isEmpty()) {
-      QueryResponse queryResponse = this.queryDatabase(relayIp, timestampStr);
+      QueryResponse queryResponse = this.queryBackend(relayIp, timestampStr);
       if (null != queryResponse) {
-        successfullyConnectedToDatabase = true;
+        successfullyConnectedToBackend = true;
         firstDate = queryResponse.firstDateInDatabase;
         lastDate = queryResponse.lastDateInDatabase;
         if (null != queryResponse.relevantStatuses
@@ -165,8 +155,8 @@ public class ExoneraTorServlet extends HttpServlet {
       this.writeFooter(out, rb, null, null);
 
     /* If we were unable to connect to the database, write an error message. */
-    } else if (!successfullyConnectedToDatabase) {
-      this.writeSummaryUnableToConnectToDatabase(out, rb);
+    } else if (!successfullyConnectedToBackend) {
+      this.writeSummaryUnableToConnectToBackend(out, rb);
       this.writeFooter(out, rb, null, null);
 
     /* Similarly, if we found the database to be empty, write an error message,
@@ -228,7 +218,10 @@ public class ExoneraTorServlet extends HttpServlet {
 
   /* Helper methods for handling the request. */
 
-  private String parseIpParameter(String passedIpParameter) {
+  /** Parse an IP parameter and return either a non-<code>null</code> value in
+   * case the parameter was valid or empty, or <code>null</code> if it was
+   * non-empty and invalid. */
+  static String parseIpParameter(String passedIpParameter) {
     String relayIp = null;
     if (passedIpParameter != null && passedIpParameter.length() > 0) {
       String ipParameter = passedIpParameter.trim();
@@ -283,8 +276,10 @@ public class ExoneraTorServlet extends HttpServlet {
     return relayIp;
   }
 
-  private String parseTimestampParameter(
-      String passedTimestampParameter) {
+  /** Parse a timestamp parameter and return either a non-<code>null</code>
+   * value in case the parameter was valid or empty, or <code>null</code> if it
+   * was non-empty and invalid. */
+  static String parseTimestampParameter(String passedTimestampParameter) {
     String timestampStr = "";
     SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
     dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@@ -304,31 +299,17 @@ public class ExoneraTorServlet extends HttpServlet {
 
   /* Helper method for fetching a query response via URL. */
 
-  private QueryResponse queryDatabase(String relayIp, String timestampStr) {
-    QueryResponse response;
+  private QueryResponse queryBackend(String relayIp, String timestampStr) {
     try (InputStreamReader isr = new InputStreamReader(new URL(
         this.exoneraTorHost + "/query.json?ip=" + relayIp + "&timestamp="
         + timestampStr).openStream())) {
-      Gson gson = new Gson();
-      response = gson.fromJson(isr, QueryResponse.class);
-      if (null == response || null == response.version) {
-        logger.warning("Response is either empty or does not contain "
-            + "version information.");
-        response = null;
-      } else if (response.version.compareTo(firstRecognizedVersion) < 0
-          || response.version.compareTo(firstUnrecognizedVersion) >= 0) {
-        logger.warning("Response has either an older or a newer version ("
-            + response.version + ") than we can handle ("
-            + firstRecognizedVersion + " <= x < " + firstUnrecognizedVersion
-            + ").");
-        response = null;
-      }
-    } catch (IOException | RuntimeException e) {
-      /* This could be anything, but the effect is that we don't have a query
-       * response to process further. */
-      response = null;
+      return QueryResponse.fromJson(isr);
+    } catch (IOException e) {
+      /* No result from backend, so that we don't have a query response to
+       * process further. */
+      logger.severe("Backend query failed: " + e.getMessage());
     }
-    return response;
+    return null;
   }
 
   /* Helper methods for writing the response. */
@@ -389,7 +370,7 @@ public class ExoneraTorServlet extends HttpServlet {
         rb.getString("form.search.label"));
   }
 
-  private void writeSummaryUnableToConnectToDatabase(PrintWriter out,
+  private void writeSummaryUnableToConnectToBackend(PrintWriter out,
       ResourceBundle rb) throws IOException {
     String contactLink =
         "<a href=\"https://www.torproject.org/about/contact\">"
@@ -592,12 +573,18 @@ public class ExoneraTorServlet extends HttpServlet {
           content = "("
               + rb.getString("technicaldetails.nickname.unknown") + ")";
         } else if (i == 4) {
-          if (content.equals("U")) {
-            content = rb.getString("technicaldetails.exit.unknown");
-          } else if (content.equals("Y")) {
-            content = rb.getString("technicaldetails.exit.yes");
-          } else {
-            content = rb.getString("technicaldetails.exit.no");
+          switch (content) {
+            case "U":
+              content = rb.getString("technicaldetails.exit.unknown");
+              break;
+            case "Y":
+              content = rb.getString("technicaldetails.exit.yes");
+              break;
+            case "N":
+              content = rb.getString("technicaldetails.exit.no");
+              break;
+            default: // should never happen
+              logger.warning("Unknown content: " + content);
           }
         }
         out.print("                <td" + attributes + ">" + content + "</td>");
diff --git a/src/main/java/org/torproject/exonerator/QueryResponse.java b/src/main/java/org/torproject/exonerator/QueryResponse.java
index 45dd017..8053e94 100644
--- a/src/main/java/org/torproject/exonerator/QueryResponse.java
+++ b/src/main/java/org/torproject/exonerator/QueryResponse.java
@@ -3,41 +3,117 @@
 
 package org.torproject.exonerator;
 
+import com.google.gson.Gson;
+import com.google.gson.annotations.Expose;
 import com.google.gson.annotations.SerializedName;
 
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Objects;
+import java.util.logging.Logger;
+
 /** Query response from the ExoneraTor database. */
 public class QueryResponse {
 
+  @Expose(serialize = false, deserialize = false)
+  private static Logger logger = Logger.getLogger(QueryResponse.class.toString());
+
+  /* Actual response format version implemented by this class. */
+  @Expose(serialize = false, deserialize = false)
+  private static final String VERSION = "1.0";
+
+  /* Don't accept query responses with versions lower than this. */
+  @Expose(serialize = false, deserialize = false)
+  private static final String FIRST_RECOGNIZED_VERSION = "1.0";
+
+  /* Don't accept query responses with this version or higher. */
+  @Expose(serialize = false, deserialize = false)
+  private static final String FIRST_UNRECOGNIZED_VERSION = "2.0";
+
   /** Version of this response format. */
-  String version = "1.0";
+  @Expose
+  String version = VERSION;
 
   /** Query IP address passed in the request; never <code>null</code>. */
+  @Expose
   @SerializedName("query_address")
   String queryAddress;
 
   /** Query date passed in the request; never <code>null</code>. */
+  @Expose
   @SerializedName("query_date")
   String queryDate;
 
   /** ISO-formatted valid-after time of the first status contained in the
    * database; only <code>null</code> if the database is empty. */
+  @Expose
   @SerializedName("first_date_in_database")
   String firstDateInDatabase;
 
   /** ISO-formatted valid-after time of the last status contained in the
    * database; only <code>null</code> if the database is empty. */
+  @Expose
   @SerializedName("last_date_in_database")
   String lastDateInDatabase;
 
   /** Whether there is at least one relevant status in the database on or within
    * a day of the requested date; <code>null</code> if the database is empty. */
+  @Expose
   @SerializedName("relevant_statuses")
   Boolean relevantStatuses;
 
   /** All matches for the given IP address and date; <code>null</code> if there
    * were no matches at all. */
+  @Expose
   Match[] matches;
 
+  /** Constructor for Gson. */
+  public QueryResponse() {}
+
+  /** Constructor for tests. */
+  QueryResponse(String version, String queryAddress, String queryDate,
+      String firstDateInDatabase, String lastDateInDatabase,
+      Boolean relevantStatuses, Match[] matches, String[] nearbyAddresses) {
+    this.version = version;
+    this.queryAddress = queryAddress;
+    this.queryDate = queryDate;
+    this.firstDateInDatabase = firstDateInDatabase;
+    this.lastDateInDatabase = lastDateInDatabase;
+    this.relevantStatuses = relevantStatuses;
+    this.matches = matches;
+    this.nearbyAddresses = nearbyAddresses;
+  }
+
+  /** Return JSON string for given QueryResponse. */
+  public static String toJson(QueryResponse response) {
+    return new Gson().toJson(response);
+  }
+
+  /** Return QueryResponse parsed from the given input stream, or
+   * <code>null</code> if something fails or an unrecognized version is found. */
+  public static QueryResponse fromJson(Reader reader) {
+    Gson gson = new Gson();
+    try {
+      QueryResponse response = gson.fromJson(reader, QueryResponse.class);
+      if (null == response || null == response.version) {
+        logger.warning("Response is either empty or does not contain "
+            + "version information.");
+        return null;
+      } else if (response.version.compareTo(FIRST_RECOGNIZED_VERSION) < 0
+          || response.version.compareTo(FIRST_UNRECOGNIZED_VERSION) >= 0) {
+        logger.warning("Response has either an older or a newer version ("
+            + response.version + ") than we can handle ("
+            + FIRST_RECOGNIZED_VERSION + " <= x < " + FIRST_UNRECOGNIZED_VERSION
+            + ").");
+        return null;
+      }
+      return response;
+    } catch (RuntimeException e) {
+      logger.severe("JSON decoding failed: " + e.getMessage());
+    }
+    return null;
+  }
+
   /** Match details. */
   static class Match {
 
@@ -56,10 +132,24 @@ public class QueryResponse {
     /** Whether this relay permitted exiting or not; <code>null</code> if
      * unknown. */
     Boolean exit;
+
+    /** Constructor for Gson. */
+    public Match() {}
+
+    /** Constructor for tests. */
+    Match(String timestamp, String[] addresses, String fingerprint,
+        String nickname, Boolean exit) {
+      this.timestamp = timestamp;
+      this.addresses = addresses;
+      this.fingerprint = fingerprint;
+      this.nickname = nickname;
+      this.exit = exit;
+    }
   }
 
   /** All known IP addresses in the same /24 or /48 network; <code>null</code>
    * if there were direct matches for the given IP address. */
+  @Expose
   @SerializedName("nearby_addresses")
   String[] nearbyAddresses;
 }





More information about the tor-commits mailing list