diff --git a/.gitignore b/.gitignore
index 4ab30699ede350e65f150d728550908f026c23e4..0d16828bb8bf27c504e4f19175e9f1dd165aea23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,25 +3,84 @@
 
 # Mac file finder metadata
 .DS_Store
+# Windows file metadata
+._*
+# Thumbnail image caches
+Thumbs.db
+ethumbs.db
+# MS Office temporary file
+~*
+# Emacs backup file
+*~
+
+# Common
+[Bb]in/
+[Bb]uild/
+[Oo]bj/
+[Oo]ut/
+[Tt]mp/
+[Xx]86/
+[Ii][Aa]32/
+[Xx]64/
+[Xx]86_64/
+[Xx]86-64/
+[Aa]rm
+[Aa]32
+[Tt]32
+[Aa]64
+*.tmp
+*.bak
+*.bk
+*.swp
+
+# Miscellaneous
+*.gcno
+
+# LaTeX files
+*.aux
+*.log
+*.tex.gz
+*.synctex.gz
+*.texmk
+*.dvi
+*.fls
+*.fdb_latexmk
+
+# C files
+*.o
+*.out
 
 # Java files
 *.class
+javadoc/
 
-# Emacs backup file
-*~
+# Maven
+target/
+
+# Python files
+*.pyc
+*.pyo
+
+# Swift files
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
 
-# IntelliJ files
+# JetBrains (IntelliJ IDEA, PyCharm, etc) files
 .idea/
-out/
+cmake-build-*/
 *.iml
 *.iws
 *.ipr
 
 # Eclipse files
-bin/
 .settings/
-.classpath
 .project
+.classpath
+.buildpath
+.loadpath
+local.properties
 
 # Visual Studio / VS Code files
 .vs*/
@@ -47,7 +106,6 @@ bin/
 *.tlb
 *.tli
 *.tlh
-*.tmp
 *.tmp_proj
 *_wpftmp.csproj
 *.log
@@ -57,6 +115,15 @@ bin/
 *.pidb
 *.svclog
 *.scc
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+*.psess
+*.vsp
+*.vspx
 
 # Netbeans files
 nbproject/private/
@@ -67,12 +134,8 @@ nbdist/
 nbactions.xml
 nb-configuration.xml
 
-# Maven
-target/
-
-# Miscellaneous
-tmp/
-*.bak
-*.bk
-*.swp
+# Xcode files
+*.xcodeproj/
+xcuserdata/
+.build/
 
diff --git a/assignment/socket_chat--patterns-and-testing.md b/assignment/socket_chat--patterns-and-testing.md
index 66f2855c06642e4c967e97c07a3022d6e74c0472..fd6df5b3707b483a576d7ccb1f3feb8b66768da4 100644
--- a/assignment/socket_chat--patterns-and-testing.md
+++ b/assignment/socket_chat--patterns-and-testing.md
@@ -1,7 +1,7 @@
 # Socket Chat Program
 
--   Assignment Due: February 10, 2020 at 9:30am
--   Peer Assessment Due: February 10, 2020 at 11:59pm
+-   Assignment Due: June 23, 2020 at 11:00am (CDT, UTC-5)
+-   Peer Assessment Due: June 23, 2020 at 11:59pm
 
 In this assignment you will modify a simple 2-way network chat program to
 prompt the user and get responses in a language other than English, and to
@@ -25,8 +25,8 @@ Students will:
     -   Simple Factory Pattern
 -   Make use of resource "property" files to support internationalization
 -   Develop whitebox test cases to attain statement coverage
+-   Practice using test doubles
 -   Create JUnit tests for those test cases
-<!-- -   Practice using test doubles -- MIGHT HAVE TO PUT DOUBLES IN A FUTURE ASSIGNMENT-->
 -   Practice overriding the default author on a git commit
 
 ## Instructions
@@ -60,7 +60,7 @@ git commit --author="Herbie Husker <herbie@huskers.unl.edu>"
     prepared for you.
 
     1.  Navigate to your shared directory
-        (<https://git.unl.edu/csce_361/spring2020/09pairNN/>, where *NN* is your
+        (<https://git.unl.edu/csce_361/summer2020/09pairNN/>, where *NN* is your
         team number).
 
     1.  Verify that the repository is private, and that you and your partner
@@ -71,7 +71,7 @@ git commit --author="Herbie Husker <herbie@huskers.unl.edu>"
     1.  Clone the project: `git clone <URL>` (here the angle brackets should
         not be included).
 
-        -   **Do *NOT* place your calculator repository inside your
+        -   **Do *NOT* place your socket_chat repository inside your
             csce361-homework repository!**
 
     1.  Import the project into your IDE. The project is set up as a Maven
@@ -105,28 +105,43 @@ authoritative source of requirements.**
 ## Assignment
 
 Look over the starter code, view
-[this short demonstration](https://use.vg/qB6zFZ), and run the program to get a
+<!--[this short demonstration](https://use.vg/qB6zFZ),-->
+[this short demonstration](TODO) <!-- this one includes relay server -->
+and run the program to get a
 feel for how the program works.
 
 -   One host, one client, but the same code
--   To avoid having to do any network discovery, the host displays its IP
-    address, which must be shared with whoever is running the client so they
-    can type in the IP address
--   The host sets the port number to be used, and this must be shared with
-    whoever is running the client so they can type in the IP address
-    -   If the port number were hard-coded then we'd run the risk of that port
-        number already being in use
--   The host sets up a `ServerSocket` to accept the client's connection, and
-    the client sets up a `Socket` to connect to the host
--   The host and client alternate turns sending Strings to each other over the
+-   Normal usage (requires host and client to be on the same network):
+    -   To avoid having to do any network discovery, the host displays its IP
+        address, which must be shared with whoever is running the client so they
+        can type in the IP address
+    -   The host sets the port number to be used, and this must be shared with
+        whoever is running the client so they can type in the IP address
+        -   If the port number were hard-coded then we'd run the risk of that
+            port number already being in use
+    -   The host sets up a `ServerSocket` to accept the client's connection, and
+        the client sets up a `Socket` to connect to the host
+-   Remote instruction usage:
+    -   We have provided a Relay Server on csce.unl.edu, whose IP address is
+        publicly visible, so it doesn't have to be on the same network as the
+        chatters
+    -   When launching SocketChat, both chatters should select the option to be
+        the client
+    -   IP address: `-127.93.-91.26` <!--`129.93.165.26`-->, Port: `361NN`,
+        where *NN* is your team number
+-   The two chatters alternate turns sending Strings to each other over the
     socket
--   When there's message from either the host or client consisting solely of
-    the keyword `EXIT` in all capital letters, the program terminates.
+    -   The host (or the first chatter to connect to the Relay Sever) sends the
+        first message
+-   When there's message from either chatter consisting solely of the keyword
+    `EXIT` in all capital letters, the program terminates.
 
+<!--
 NOTE: in the demonstration, I used jar files. This was convenient for the
 purposes of the demonstration. You do not have to create jar files, but if you
 want to, you can use Maven's "package" target. (Separate instructions for this
 will be posted on Piazza.)
+-->
 
 ### Internationalization
 
@@ -141,7 +156,7 @@ Notice that anyplace that the program outputs something, the argument to
 that corresponds to the specified key String. We also use this to compare the
 user input to a string.
 
-The key-value pairs are in a properties file in the `.../resources/` directory.
+The key-value pairs are in a properties file in the `resources/` directory.
 The files are of the form `basename_XX.properties`, where XX is the 2- or 3-
 character language code. Example language codes are `en` for English, `fr` for
 French, `de` for German, `zh` for Chinese, and `tlh` for Klingon. If you decide
@@ -198,12 +213,46 @@ parameterized strings similar to C's `printf()`.
         -   Otherwise, create a `Locale` using [`Locale.Builder`](https://docs.oracle.com/javase/8/docs/api/java/util/Locale.Builder.html).
 <!--            (Yes, this is using the Builder Pattern.) -->
 
+### Whitebox Testing
+
+This is the specification for `Chat.communicateOneMessage()` (not including
+exception handling):
+-   The method will return `true` unless otherwise noted below
+-   If the message to be processed originates locally then the contents of
+    `localInput` shall be enciphered and be placed on `remoteOutput`
+-   If the message to be processed originates remotely then the contents of
+    `remoteInput` shall be deciphered and be placed on `localOutput`
+-   If a remotely-originated message is `null`, then an error message shall be
+    placed on `localOutput` indicating that a null message was received, and
+    the method will return `false`
+-   If the message, regardless of origin, consists solely of a keyword, then
+    the message shall be passed to the `handleKeyword()` method for processing;
+    the `communicateOneMessage()` method will return the boolean value returned
+    by `handleKeyword()`
+
+4.  Write sufficient JUnit tests to attain *statement coverage* of
+    `communicateOneMessage()`
+
+    -   You will need to prepare test doubles to be able to run automated tests
+        of `communicateOneMessage()`
+
+    -   The exception-handling behavior is not part of the specification, but
+        the developers wisely attempted to gracefully handle exceptional
+        conditions; you will need to write tests to exercise the catch block
+        for a `SocketException` (we have provided a test for the `IOException`
+        catch block)
+
+    -   You can confirm that you have statement coverage by generating a
+        coverage report
+        -   Eclipse: <https://www.eclemma.org/userdoc/importexport.html>
+            -   Instructions at the bottom of that page. Export as HTML
+        -   IntelliJ IDEA: <https://www.jetbrains.com/help/idea/generating-code-coverage-report.html>
+
 ### Strategy Pattern
 
 You will use the *Strategy Pattern* to attach cipher algorithms to the chat
 program.[^1]
 
--   Figure 16.9 on Kung p397
 -   HFDP, [Chapter 1](https://learning.oreilly.com/library/view/head-first-design/0596007124/ch01.html)
 
 [^1]:   For this application, you are going to write cipher algorithms and
@@ -213,18 +262,15 @@ program.[^1]
         `javax.crypto.Cipher` and, if you're streaming text back and forth as
         in this program, attach the algorithms using the Decorator pattern via
         `javax.crypto.CipherInputStream` and `javax.crypto.CipherOutputStream`.
-
-    Decorator Pattern:
-
-    -   Figure 16.34 on Kung p421
-    -   HFDP, [Chapter 3](https://learning.oreilly.com/library/view/head-first-design/0596007124/ch03.html)
+        (for the Decorator Pattern, see HFDP,
+        [Chapter 3](https://learning.oreilly.com/library/view/head-first-design/0596007124/ch03.html))
 
 The original version of this program sends plaintext messages over the network.
 The current version passes an outgoing message through `Chat.encipher()` and
 incoming messages through `Chat.decipher()`. Right now, all these methods do is
 return the original message without enciphering (or deciphering) it.
 
-4.  Create `Cipher.java`, an interface with two methods: `String
+5.  Create `Cipher.java`, an interface with two methods: `String
     encipher(String plaintext)` whose specification is that it that passes the
     plaintext through a cipher to create ciphertext, and `String
     decipher(String ciphertext)` whose specification is that it passes the
@@ -266,7 +312,9 @@ internally, you've delegated the behavior for `Chat.encipher()` and
 This would be a good time to verify that your partial implementation of the
 Strategy Pattern hasn't broken anything.
 
-8.  Implement two classical ciphers as Java classes that implement the `Cipher`
+9.  Re-run your test suite to make sure no changes caused your tests to fail.
+
+1.  Implement two classical ciphers as Java classes that implement the `Cipher`
     interface. You can include any other methods you feel are necessary;
     however, only `encipher()` and `decipher()` will be exposed to `Chat.java`.
     -   You may use ciphers you implemented in a previous course (but they must
@@ -285,6 +333,21 @@ Strategy Pattern hasn't broken anything.
 1.  You can replace `NullCipher` in the `Chat` constructor with either of the
     other ciphers you wrote, and your program will still work.
 
+1.  If `NullCipher` is no longer the initial cipher then make any changes to
+    your tests that are necessary to reflect this new behavior
+    -   The option that requires the least effort in the short term would be to
+        change the input and/or expected output for some tests to take into
+        account the new cipher
+    -   A better option would be to make any changes needed to your code to
+        allow your tests to set the cipher being used, then set the cipher
+        either in your tests or in your `setUp()`, and finally change the input
+        and/or expected output for some tests to take into account the assigned
+        cipher
+        -   While this will require a little more effort in the short term, it
+            means you won't have to change your tests again if you (or somebody
+            else) later decides to change the initial cipher
+    -   Run your test suite to make sure everything passes
+
 What we need now to complete the Strategy Pattern is a way to replace the
 cipher algorithm at runtime. Many options are possible; we'll use the...
 
@@ -310,7 +373,7 @@ the client code cannot assume a concrete type.
 
 We will use a parameterized factory.
 
-11. Create `CipherFactory.java`.
+14. Create `CipherFactory.java`.
 
 1.  Write the method
     `public static Cipher createCipher(String name, String[] keys)`. This
@@ -325,23 +388,11 @@ We will use a parameterized factory.
     arguments. This method shall return a default cipher algorithm. (You decide
     what that default is).
 
-1.  Write JUnit tests to verify that `createCipher(String, String[])` returns
-    the correct type of `Cipher`. Hint: Java's `instanceof` operator evaluates
-    to `true` when the first argument is an instance of the class specified by
-    the second argument. Write enough tests to attain *statement coverage* of
-    `createCipher(String, String[])`.
-
-    -   You can confirm that you have statement coverage by generating a
-        coverage report.
-        -   Eclipse: <https://www.eclemma.org/userdoc/importexport.html>
-            -   Instructions at the bottom of that page. Export as HTML.
-        -   IntelliJ IDEA: <https://www.jetbrains.com/help/idea/generating-code-coverage-report.html>
-
 That's it. That's your Simple Factory.
 
 Now it's time to use it.
 
-15. Find line line in the `Chat.Chat()` constructor where you wrote
+17. Find line line in the `Chat.Chat()` constructor where you wrote
     `cipherBehavior = new NullCipher()` (or something like that). Replace
     `new NullCipher()` with `CipherFactory.createCipher()`. Now your cipher
     algorithm is whatever the default happens to be.
@@ -373,6 +424,8 @@ Now it's time to use it.
         `CipherFactory.java`. If you have to add or modify `Chat.java` then
         your code still depends on knowledge of `Cipher` implementations.
 
+1.  Re-run your test suite to make sure no changes caused your tests to fail.
+
 You can now chat away without worrying about a "l337 h4x0r" being snoopy. (If
 you're worried about someone with NSA-level snooping capabilities, don't use a
 "classical era" cipher! They're relatively easy to break in general, and the
@@ -409,6 +462,7 @@ is due, and we will look in the Maven-conventional directories for:
 -   `Cipher.java` and `CipherFactory.java`
 -   Three `Cipher` implementations (`NullCipher.java` plus two others of your
     choosing)
+-   Updated `ChatTest.java`
 
 *It is your responsibility to ensure that your work is in the master branch of
 the **correct repository** and that we have the correct level of access to the
@@ -418,7 +472,7 @@ the correct repository, or that we cannot access, will not be graded.*
 
 ## Rubric
 
-The assignment is worth **24 points**:
+The assignment is worth **25 points**:
 
 -   **4 points** for internationalization
     -   1 points for creating the properties file for the other language
@@ -430,16 +484,16 @@ The assignment is worth **24 points**:
     -   1 point for creating `Cipher.java` and `NullCipher.java`
     -   2 points each for the 2 other classical cipher algorithms
     -   1 point for delegating `encipher` and `decipher`
-    -   3 points for writing the code to change cipher algorithms
+    -   3 points for writing the code to change cipher algorithms that is fully
+        independent of `Cipher` implementations
 
--   **5 points** for tests
+-   **6 points** for tests
     -   2 point for testing `encipher` and `decipher` for your two classical
         cipher algorithms
-    -   3 points for writing sufficient tests to attain statement coverage of
-        `CipherFactory.createCipher(String, String[])`
+    -   4 points for writing sufficient tests to attain statement coverage of
+        `Chat.communicateOneMessage()`
 
--   **1 point** for making regular commits; *i.e.*, not waiting until the end
-    of the project to make a massive commit.
+-   **1 point** for making regular commits throughout the project
 
 -   **2 points** for meaningful commit messages
 
@@ -456,8 +510,9 @@ complicit in their academic dishonesty.*
 
 The contribution is worth **10 points**:
 
--   **1 point** for completing peer assessment
--   **5 points** for equitable contribution based on peer assessments
+-   **1 point** for completing peer assessment on time
+-   **1 point** for contacting your partner promptly
+-   **4 points** for equitable contribution based on peer assessments
 -   **4 points** for equitable contribution based on git history
 
 ## Footnote
diff --git a/src/main/java/edu/unl/cse/csce361/socket_chat/Chat.java b/src/main/java/edu/unl/cse/csce361/socket_chat/Chat.java
index 579409050689facea7002d5db3c9a6b3af374735..478514a1f52c3a85edeee1dffda52211de02442e 100644
--- a/src/main/java/edu/unl/cse/csce361/socket_chat/Chat.java
+++ b/src/main/java/edu/unl/cse/csce361/socket_chat/Chat.java
@@ -60,7 +60,7 @@ public class Chat {
         } catch (IOException ioException) {
             // "Connection failed: ..."
             System.err.println(bundle.getString("connection.error.generalConnectionFailure") + ": " + ioException);
-            System.exit(1);
+            exit(1);
         }
         this.socket = socket;
         return socket;
@@ -145,7 +145,7 @@ public class Chat {
                 } else {
                     // "Exceeded maximum number of connection attempts. Terminating."
                     System.err.println(bundle.getString("connection.error.tooManyAttempts"));
-                    System.exit(1);
+                    exit(1);
                 }
             }
         } while (socket == null);
@@ -255,7 +255,7 @@ public class Chat {
             // "Terminating."
             System.err.println(bundle.getString("communicate.error.cannotSetUpStreams") + ": " + ioException);
             System.err.println(bundle.getString("communicate.info.terminating"));
-            System.exit(1);
+            exit(1);
         }
     }
 
@@ -322,8 +322,8 @@ public class Chat {
                 keepTalking = handleKeyword(message, localMessage, localInput, localOutput);
             }
         } catch (IOException ioException) {
-            System.err.println("Connection dropped: " + ioException);
-            System.exit(1);
+            localOutput.println("Connection dropped: " + ioException);
+            exit(1);
         }
         return keepTalking;
     }
@@ -363,6 +363,10 @@ public class Chat {
         return true;
     }
 
+    protected void exit(int exitCode) {
+        System.exit(exitCode);
+    }
+
     private String encipher(String plaintext) {
         String ciphertext = plaintext;      // Replace this with a call to cipherBehavior.encipher(plaintext)
         return ciphertext;
diff --git a/src/test/java/edu/unl/cse/csce361/socket_chat/ChatTest.java b/src/test/java/edu/unl/cse/csce361/socket_chat/ChatTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..aa041cb022ad00557f0bfd6599532c5ae0f99ddf
--- /dev/null
+++ b/src/test/java/edu/unl/cse/csce361/socket_chat/ChatTest.java
@@ -0,0 +1,64 @@
+package edu.unl.cse.csce361.socket_chat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.*;
+import java.net.SocketException;
+import java.util.ResourceBundle;
+
+import static org.junit.Assert.*;
+
+public class ChatTest {
+    static class MockChat extends Chat {
+        boolean exitCalled = false;
+        int actualExitCode = 0;
+
+        @Override
+        protected void exit(int exitCode) {
+            exitCalled = true;
+            actualExitCode = exitCode;
+        }
+    }
+
+    private MockChat chatter;
+    private String newLine;
+
+    private BufferedReader remoteInput;
+    private PrintStream localOutput;
+    private ByteArrayOutputStream localOutputSpy;
+    private ResourceBundle bundle;
+
+    @Before
+    public void setUp() {
+        chatter = new MockChat();
+        newLine = System.getProperty("line.separator");
+        bundle = ResourceBundle.getBundle("socketchat");
+        localOutputSpy = new ByteArrayOutputStream();
+        localOutput = new PrintStream(localOutputSpy);
+    }
+
+    @Test
+    public void testCommunicateOneMessageCatchesIOException() {
+        // arrange
+        Reader stubReader = new Reader() {
+            @Override
+            public int read(char[] cbuf, int off, int len) throws IOException {
+                throw new IOException("IOException from stubReader");
+            }
+
+            @Override
+            public void close() throws IOException {
+            }
+        };
+        remoteInput = new BufferedReader(stubReader);
+        int expectedExitCode = 1;
+        String expectedOutput = "Connection dropped: java.io.IOException: IOException from stubReader" + newLine;
+        // act
+        boolean actualReturn = chatter.communicateOneMessage(null, remoteInput, localOutput, null, false);
+        // assert
+        assertTrue(chatter.exitCalled);
+        assertEquals(expectedExitCode, chatter.actualExitCode);
+        assertEquals(expectedOutput, localOutputSpy.toString());
+    }
+}
\ No newline at end of file