Skip to content
Snippets Groups Projects
Commit 51c3da83 authored by Christopher Bohn's avatar Christopher Bohn :thinking:
Browse files

Replaced testing of CipherFactory with testing of communicateOneMessage

parent e1eb9caf
No related branches found
No related tags found
No related merge requests found
...@@ -3,25 +3,84 @@ ...@@ -3,25 +3,84 @@
# Mac file finder metadata # Mac file finder metadata
.DS_Store .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 # Java files
*.class *.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/ .idea/
out/ cmake-build-*/
*.iml *.iml
*.iws *.iws
*.ipr *.ipr
# Eclipse files # Eclipse files
bin/
.settings/ .settings/
.classpath
.project .project
.classpath
.buildpath
.loadpath
local.properties
# Visual Studio / VS Code files # Visual Studio / VS Code files
.vs*/ .vs*/
...@@ -47,7 +106,6 @@ bin/ ...@@ -47,7 +106,6 @@ bin/
*.tlb *.tlb
*.tli *.tli
*.tlh *.tlh
*.tmp
*.tmp_proj *.tmp_proj
*_wpftmp.csproj *_wpftmp.csproj
*.log *.log
...@@ -57,6 +115,15 @@ bin/ ...@@ -57,6 +115,15 @@ bin/
*.pidb *.pidb
*.svclog *.svclog
*.scc *.scc
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
*.psess
*.vsp
*.vspx
# Netbeans files # Netbeans files
nbproject/private/ nbproject/private/
...@@ -67,12 +134,8 @@ nbdist/ ...@@ -67,12 +134,8 @@ nbdist/
nbactions.xml nbactions.xml
nb-configuration.xml nb-configuration.xml
# Maven # Xcode files
target/ *.xcodeproj/
xcuserdata/
# Miscellaneous .build/
tmp/
*.bak
*.bk
*.swp
# Socket Chat Program # Socket Chat Program
- Assignment Due: February 10, 2020 at 9:30am - Assignment Due: June 23, 2020 at 11:00am (CDT, UTC-5)
- Peer Assessment Due: February 10, 2020 at 11:59pm - Peer Assessment Due: June 23, 2020 at 11:59pm
In this assignment you will modify a simple 2-way network chat program to 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 prompt the user and get responses in a language other than English, and to
...@@ -25,8 +25,8 @@ Students will: ...@@ -25,8 +25,8 @@ Students will:
- Simple Factory Pattern - Simple Factory Pattern
- Make use of resource "property" files to support internationalization - Make use of resource "property" files to support internationalization
- Develop whitebox test cases to attain statement coverage - Develop whitebox test cases to attain statement coverage
- Practice using test doubles
- Create JUnit tests for those test cases - 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 - Practice overriding the default author on a git commit
## Instructions ## Instructions
...@@ -60,7 +60,7 @@ git commit --author="Herbie Husker <herbie@huskers.unl.edu>" ...@@ -60,7 +60,7 @@ git commit --author="Herbie Husker <herbie@huskers.unl.edu>"
prepared for you. prepared for you.
1. Navigate to your shared directory 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). team number).
1. Verify that the repository is private, and that you and your partner 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>" ...@@ -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 1. Clone the project: `git clone <URL>` (here the angle brackets should
not be included). not be included).
- **Do *NOT* place your calculator repository inside your - **Do *NOT* place your socket_chat repository inside your
csce361-homework repository!** csce361-homework repository!**
1. Import the project into your IDE. The project is set up as a Maven 1. Import the project into your IDE. The project is set up as a Maven
...@@ -105,28 +105,43 @@ authoritative source of requirements.** ...@@ -105,28 +105,43 @@ authoritative source of requirements.**
## Assignment ## Assignment
Look over the starter code, view 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. feel for how the program works.
- One host, one client, but the same code - One host, one client, but the same code
- 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 - 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 address, which must be shared with whoever is running the client so they
can type in the IP address can type in the IP address
- The host sets the port number to be used, and this must be shared with - 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 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 - If the port number were hard-coded then we'd run the risk of that
number already being in use port number already being in use
- The host sets up a `ServerSocket` to accept the client's connection, and - 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 client sets up a `Socket` to connect to the host
- The host and client alternate turns sending Strings to each other over the - 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 socket
- When there's message from either the host or client consisting solely of - The host (or the first chatter to connect to the Relay Sever) sends the
the keyword `EXIT` in all capital letters, the program terminates. 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 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 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 want to, you can use Maven's "package" target. (Separate instructions for this
will be posted on Piazza.) will be posted on Piazza.)
-->
### Internationalization ### Internationalization
...@@ -141,7 +156,7 @@ Notice that anyplace that the program outputs something, the argument to ...@@ -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 that corresponds to the specified key String. We also use this to compare the
user input to a string. 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- 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 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 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()`. ...@@ -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). - 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.) --> <!-- (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 ### Strategy Pattern
You will use the *Strategy Pattern* to attach cipher algorithms to the chat You will use the *Strategy Pattern* to attach cipher algorithms to the chat
program.[^1] program.[^1]
- Figure 16.9 on Kung p397
- HFDP, [Chapter 1](https://learning.oreilly.com/library/view/head-first-design/0596007124/ch01.html) - 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 [^1]: For this application, you are going to write cipher algorithms and
...@@ -213,18 +262,15 @@ program.[^1] ...@@ -213,18 +262,15 @@ program.[^1]
`javax.crypto.Cipher` and, if you're streaming text back and forth as `javax.crypto.Cipher` and, if you're streaming text back and forth as
in this program, attach the algorithms using the Decorator pattern via in this program, attach the algorithms using the Decorator pattern via
`javax.crypto.CipherInputStream` and `javax.crypto.CipherOutputStream`. `javax.crypto.CipherInputStream` and `javax.crypto.CipherOutputStream`.
(for the Decorator Pattern, see HFDP,
Decorator Pattern: [Chapter 3](https://learning.oreilly.com/library/view/head-first-design/0596007124/ch03.html))
- Figure 16.34 on Kung p421
- 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 original version of this program sends plaintext messages over the network.
The current version passes an outgoing message through `Chat.encipher()` and The current version passes an outgoing message through `Chat.encipher()` and
incoming messages through `Chat.decipher()`. Right now, all these methods do is incoming messages through `Chat.decipher()`. Right now, all these methods do is
return the original message without enciphering (or deciphering) it. 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 encipher(String plaintext)` whose specification is that it that passes the
plaintext through a cipher to create ciphertext, and `String plaintext through a cipher to create ciphertext, and `String
decipher(String ciphertext)` whose specification is that it passes the decipher(String ciphertext)` whose specification is that it passes the
...@@ -266,7 +312,9 @@ internally, you've delegated the behavior for `Chat.encipher()` and ...@@ -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 This would be a good time to verify that your partial implementation of the
Strategy Pattern hasn't broken anything. 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; interface. You can include any other methods you feel are necessary;
however, only `encipher()` and `decipher()` will be exposed to `Chat.java`. however, only `encipher()` and `decipher()` will be exposed to `Chat.java`.
- You may use ciphers you implemented in a previous course (but they must - You may use ciphers you implemented in a previous course (but they must
...@@ -285,6 +333,21 @@ Strategy Pattern hasn't broken anything. ...@@ -285,6 +333,21 @@ Strategy Pattern hasn't broken anything.
1. You can replace `NullCipher` in the `Chat` constructor with either of the 1. You can replace `NullCipher` in the `Chat` constructor with either of the
other ciphers you wrote, and your program will still work. 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 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... cipher algorithm at runtime. Many options are possible; we'll use the...
...@@ -310,7 +373,7 @@ the client code cannot assume a concrete type. ...@@ -310,7 +373,7 @@ the client code cannot assume a concrete type.
We will use a parameterized factory. We will use a parameterized factory.
11. Create `CipherFactory.java`. 14. Create `CipherFactory.java`.
1. Write the method 1. Write the method
`public static Cipher createCipher(String name, String[] keys)`. This `public static Cipher createCipher(String name, String[] keys)`. This
...@@ -325,23 +388,11 @@ We will use a parameterized factory. ...@@ -325,23 +388,11 @@ We will use a parameterized factory.
arguments. This method shall return a default cipher algorithm. (You decide arguments. This method shall return a default cipher algorithm. (You decide
what that default is). 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. That's it. That's your Simple Factory.
Now it's time to use it. 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 `cipherBehavior = new NullCipher()` (or something like that). Replace
`new NullCipher()` with `CipherFactory.createCipher()`. Now your cipher `new NullCipher()` with `CipherFactory.createCipher()`. Now your cipher
algorithm is whatever the default happens to be. algorithm is whatever the default happens to be.
...@@ -373,6 +424,8 @@ Now it's time to use it. ...@@ -373,6 +424,8 @@ Now it's time to use it.
`CipherFactory.java`. If you have to add or modify `Chat.java` then `CipherFactory.java`. If you have to add or modify `Chat.java` then
your code still depends on knowledge of `Cipher` implementations. 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 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 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 "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: ...@@ -409,6 +462,7 @@ is due, and we will look in the Maven-conventional directories for:
- `Cipher.java` and `CipherFactory.java` - `Cipher.java` and `CipherFactory.java`
- Three `Cipher` implementations (`NullCipher.java` plus two others of your - Three `Cipher` implementations (`NullCipher.java` plus two others of your
choosing) choosing)
- Updated `ChatTest.java`
*It is your responsibility to ensure that your work is in the master branch of *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 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.* ...@@ -418,7 +472,7 @@ the correct repository, or that we cannot access, will not be graded.*
## Rubric ## Rubric
The assignment is worth **24 points**: The assignment is worth **25 points**:
- **4 points** for internationalization - **4 points** for internationalization
- 1 points for creating the properties file for the other language - 1 points for creating the properties file for the other language
...@@ -430,16 +484,16 @@ The assignment is worth **24 points**: ...@@ -430,16 +484,16 @@ The assignment is worth **24 points**:
- 1 point for creating `Cipher.java` and `NullCipher.java` - 1 point for creating `Cipher.java` and `NullCipher.java`
- 2 points each for the 2 other classical cipher algorithms - 2 points each for the 2 other classical cipher algorithms
- 1 point for delegating `encipher` and `decipher` - 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 - 2 point for testing `encipher` and `decipher` for your two classical
cipher algorithms cipher algorithms
- 3 points for writing sufficient tests to attain statement coverage of - 4 points for writing sufficient tests to attain statement coverage of
`CipherFactory.createCipher(String, String[])` `Chat.communicateOneMessage()`
- **1 point** for making regular commits; *i.e.*, not waiting until the end - **1 point** for making regular commits throughout the project
of the project to make a massive commit.
- **2 points** for meaningful commit messages - **2 points** for meaningful commit messages
...@@ -456,8 +510,9 @@ complicit in their academic dishonesty.* ...@@ -456,8 +510,9 @@ complicit in their academic dishonesty.*
The contribution is worth **10 points**: The contribution is worth **10 points**:
- **1 point** for completing peer assessment - **1 point** for completing peer assessment on time
- **5 points** for equitable contribution based on peer assessments - **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 - **4 points** for equitable contribution based on git history
## Footnote ## Footnote
...@@ -60,7 +60,7 @@ public class Chat { ...@@ -60,7 +60,7 @@ public class Chat {
} catch (IOException ioException) { } catch (IOException ioException) {
// "Connection failed: ..." // "Connection failed: ..."
System.err.println(bundle.getString("connection.error.generalConnectionFailure") + ": " + ioException); System.err.println(bundle.getString("connection.error.generalConnectionFailure") + ": " + ioException);
System.exit(1); exit(1);
} }
this.socket = socket; this.socket = socket;
return socket; return socket;
...@@ -145,7 +145,7 @@ public class Chat { ...@@ -145,7 +145,7 @@ public class Chat {
} else { } else {
// "Exceeded maximum number of connection attempts. Terminating." // "Exceeded maximum number of connection attempts. Terminating."
System.err.println(bundle.getString("connection.error.tooManyAttempts")); System.err.println(bundle.getString("connection.error.tooManyAttempts"));
System.exit(1); exit(1);
} }
} }
} while (socket == null); } while (socket == null);
...@@ -255,7 +255,7 @@ public class Chat { ...@@ -255,7 +255,7 @@ public class Chat {
// "Terminating." // "Terminating."
System.err.println(bundle.getString("communicate.error.cannotSetUpStreams") + ": " + ioException); System.err.println(bundle.getString("communicate.error.cannotSetUpStreams") + ": " + ioException);
System.err.println(bundle.getString("communicate.info.terminating")); System.err.println(bundle.getString("communicate.info.terminating"));
System.exit(1); exit(1);
} }
} }
...@@ -322,8 +322,8 @@ public class Chat { ...@@ -322,8 +322,8 @@ public class Chat {
keepTalking = handleKeyword(message, localMessage, localInput, localOutput); keepTalking = handleKeyword(message, localMessage, localInput, localOutput);
} }
} catch (IOException ioException) { } catch (IOException ioException) {
System.err.println("Connection dropped: " + ioException); localOutput.println("Connection dropped: " + ioException);
System.exit(1); exit(1);
} }
return keepTalking; return keepTalking;
} }
...@@ -363,6 +363,10 @@ public class Chat { ...@@ -363,6 +363,10 @@ public class Chat {
return true; return true;
} }
protected void exit(int exitCode) {
System.exit(exitCode);
}
private String encipher(String plaintext) { private String encipher(String plaintext) {
String ciphertext = plaintext; // Replace this with a call to cipherBehavior.encipher(plaintext) String ciphertext = plaintext; // Replace this with a call to cipherBehavior.encipher(plaintext)
return ciphertext; return ciphertext;
......
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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment