Index: /applications/editors/josm/plugins/MicrosoftStreetside/.classpath
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/.classpath	(revision 34385)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/.classpath	(revision 34386)
@@ -1,15 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <classpath>
-	<classpathentry excluding="config/" including="data/**|images/**|LICENSE|LICENSE_*" kind="src" output="bin/main" path="">
+	<classpathentry including="data/**|images/**|LICENSE|LICENSE_*" kind="src" output="bin/main" path="">
 		<attributes>
 			<attribute name="gradle_scope" value="main"/>
 			<attribute name="gradle_used_by_scope" value="main,test"/>
-		</attributes>
-	</classpathentry>
-	<classpathentry kind="src" path="config"/>
-	<classpathentry kind="src" output="bin/minJosmVersion" path="src">
-		<attributes>
-			<attribute name="gradle_scope" value="minJosmVersion"/>
-			<attribute name="gradle_used_by_scope" value="minJosmVersion"/>
 		</attributes>
 	</classpathentry>
@@ -18,4 +11,10 @@
 			<attribute name="gradle_scope" value="test"/>
 			<attribute name="gradle_used_by_scope" value="test"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="src" output="bin/minJosmVersion" path="src">
+		<attributes>
+			<attribute name="gradle_scope" value="minJosmVersion"/>
+			<attribute name="gradle_used_by_scope" value="minJosmVersion"/>
 		</attributes>
 	</classpathentry>
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/README
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/README	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/README	(revision 34386)
@@ -0,0 +1,9 @@
+
+This directory includes the test resources for the streetside plugin.
+
+
+unit/            test sources (Unit tests, functional tests)
+
+config/          configuration files for running tests
+
+data/            test data
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/requests/changeset.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/requests/changeset.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/requests/changeset.json	(revision 34386)
@@ -0,0 +1,40 @@
+{
+  "type": "location",
+  "changes": [
+    {
+      "image_key": "wMAqAFr3xE9072G8Al6WLQ",
+      "to": {
+        "geometry": {
+          "coordinates": [13.3323, 50.44612],
+          "type": "Point"
+        },
+        "properties": {"ca": 273.3},
+        "type": "Feature"
+      }
+    },
+    {
+      "image_key": "7erPn382xDMtmfdh0xtvUw",
+      "to": {
+        "geometry": {
+          "coordinates": [13.3328, 50.44619],
+          "type": "Point"
+        },
+        "properties": {},
+        "type": "Feature"
+      }
+    },
+    {
+      "image_key": "31KDbCOzla0fJBtIeoBr1A",
+      "to": {
+        "properties": {"ca": 13.4}
+      }
+    },
+    {
+      "image_key": "invalid image key will be ignored",
+      "to": {
+        "properties": {"ca": 13.4}
+      }
+    }
+  ],
+  "request_comment": "JOSM-created"
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/README.md
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/README.md	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/README.md	(revision 34386)
@@ -0,0 +1,3 @@
+This directory contains example data representative for what the APIv3 typically returns for certain queries.
+
+The examples are taken from https://www.mapillary.com/developer/api-documentation/ .
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/imageDetection.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/imageDetection.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/imageDetection.json	(revision 34386)
@@ -0,0 +1,45 @@
+{
+  "type": "Feature",
+  "properties": {
+    "area": 0.0015604496002197266,
+    "image_key": "QhRcdlGS_Rn_a1_HTclefg",
+    "key": "gn0llgitnnuqonecevbmf52ino",
+    "package": "trafficsign",
+    "score": 0.710661225175,
+    "shape": {
+      "type": "Polygon",
+      "coordinates": [
+        [
+          [
+            0.330078125,
+            0.466064453125
+          ],
+          [
+            0.3642578125,
+            0.466064453125
+          ],
+          [
+            0.3642578125,
+            0.51171875
+          ],
+          [
+            0.330078125,
+            0.51171875
+          ],
+          [
+            0.330078125,
+            0.466064453125
+          ]
+        ]
+      ]
+    },
+    "value": "regulatory--no-overtaking-by-heavy-goods-vehicles--g1"
+  },
+  "geometry": {
+    "type": "Point",
+    "coordinates": [
+      10.805287,
+      55.321409
+    ]
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/mapObject.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/mapObject.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/mapObject.json	(revision 34386)
@@ -0,0 +1,21 @@
+{
+  "type": "Feature",
+  "properties": {
+    "accuracy": 1,
+    "altitude": 1.7983143,
+    "first_seen_at": "2016-07-01T12:49:08.553Z",
+    "key": "9f3tl0z2xanom2inyyks65negx",
+    "last_seen_at": "2016-07-01T12:49:08.553Z",
+    "package": "trafficsign",
+    "updated_at": "2017-02-08T15:02:03.778Z",
+    "value": "regulatory--no-entry--g1",
+    "detections": []
+  },
+  "geometry": {
+    "type": "Point",
+    "coordinates": [
+      13.017088890075684,
+      55.60746765136719
+    ]
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImageDetections.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImageDetections.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImageDetections.json	(revision 34386)
@@ -0,0 +1,95 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "properties": {
+        "area": 0.00010585784912109375,
+        "image_key": "33zgql54_tBVvmIij0zrcA",
+        "key": "bzqdn10wz1s1xd3lae3hawgja0",
+        "package": "trafficsign",
+        "score": 0.000001,
+        "shape": {
+          "type": "Polygon",
+          "coordinates": [
+            [
+              [
+                0.42724609375,
+                0.69091796875
+              ],
+              [
+                0.436279296875,
+                0.69091796875
+              ],
+              [
+                0.436279296875,
+                0.70263671875
+              ],
+              [
+                0.42724609375,
+                0.70263671875
+              ],
+              [
+                0.42724609375,
+                0.69091796875
+              ]
+            ]
+          ]
+        },
+        "value": "information--pedestrians-crossing--g1"
+      },
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          12.995127,
+          55.550739
+        ]
+      }
+    },
+    {
+      "type": "Feature",
+      "properties": {
+        "area": 0.00010585784912109375,
+        "image_key": "33zgql54_tBVvmIij0zrcA",
+        "key": "uzve1xkyk5qbjwrzaq0do09u1x",
+        "package": "trafficsign",
+        "score": 0.000001,
+        "shape": {
+          "type": "Polygon",
+          "coordinates": [
+            [
+              [
+                0.42724609375,
+                0.69091796875
+              ],
+              [
+                0.436279296875,
+                0.69091796875
+              ],
+              [
+                0.436279296875,
+                0.70263671875
+              ],
+              [
+                0.42724609375,
+                0.70263671875
+              ],
+              [
+                0.42724609375,
+                0.69091796875
+              ]
+            ]
+          ]
+        },
+        "value": "information--pedestrians-crossing--g1"
+      },
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          12.995127,
+          55.550739
+        ]
+      }
+    }
+  ]
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImages.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImages.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchImages.json	(revision 34386)
@@ -0,0 +1,43 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "properties": {
+        "ca": 232.73019999999997,
+        "camera_make": "Apple",
+        "captured_at": "2017-04-10T05:51:30.334Z",
+        "key": "_yA5uXuSNugmsK5VucU6Bg",
+        "pano": false,
+        "user_key": "UtczWn8y3afb0GQVW-AiOQ",
+        "username": "xtyou"
+      },
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.215628000000038,
+          48.90262200000001
+        ]
+      }
+    },
+    {
+      "type": "Feature",
+      "properties": {
+        "ca": 237.21129999999994,
+        "camera_make": "Apple",
+        "captured_at": "2017-04-10T05:51:26.853Z",
+        "key": "nmF-Wq4EvVTgAUmBicSCCg",
+        "pano": false,
+        "user_key": "UtczWn8y3afb0GQVW-AiOQ",
+        "username": "xtyou"
+      },
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          2.2156680000000506,
+          48.90267399999999
+        ]
+      }
+    }
+  ]
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchMapObjects.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchMapObjects.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchMapObjects.json	(revision 34386)
@@ -0,0 +1,31 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "properties": {
+        "accuracy": 1,
+        "altitude": 3.688496,
+        "first_seen_at": "2016-10-16T09:42:56.060Z",
+        "key": "qpku21qv8rjn7fll1v671732th",
+        "last_seen_at": "2016-10-16T09:42:56.060Z",
+        "package": "trafficsign",
+        "updated_at": "2016-11-29T12:21:22.275Z",
+        "value": "regulatory--no-parking--g1",
+        "detections": [
+          {
+            "detection_key": "cpatpdftmogffmhihau9792tua",
+            "image_key": "bsw3H-ajJD42zZSg2P64hA"
+          }
+        ]
+      },
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+          13.005650520324707,
+          55.608367919921875
+        ]
+      }
+    }
+  ]
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchSequences.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchSequences.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/searchSequences.json	(revision 34386)
@@ -0,0 +1,51 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "properties": {
+        "camera_make": "Apple",
+        "captured_at": "2016-03-14T13:44:53.860Z",
+        "created_at": "2016-03-17T10:47:53.106Z",
+        "coordinateProperties": {
+          "cas": [
+            323.0319999999999,
+            320.8918,
+            333.62239999999997,
+            329.94820000000004
+          ],
+          "image_keys": [
+            "LwrHXqFRN_pszCopTKHF_Q",
+            "Aufjv2hdCKwg9LySWWVSwg",
+            "QEVZ1tp-PmrwtqhSwdW9fQ",
+            "G_SIwxNcioYeutZuA8Rurw"
+          ]
+        },
+        "key": "LMlIPUNhaj24h_q9v4ArNw",
+        "pano": false,
+        "user_key": "AGfe-07BEJX0-kxpu9J3rA"
+      },
+      "geometry": {
+        "type": "LineString",
+        "coordinates": [
+          [
+            16.432958,
+            7.246497
+          ],
+          [
+            16.432955,
+            7.246567
+          ],
+          [
+            16.432971,
+            7.248372
+          ],
+          [
+            16.432976,
+            7.249027
+          ]
+        ]
+      }
+    }
+  ]
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/sequence.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/sequence.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/sequence.json	(revision 34386)
@@ -0,0 +1,34 @@
+{
+  "type": "Feature",
+  "properties": {
+    "camera_make": "Apple",
+    "captured_at": "2016-03-14T13:44:37.206Z",
+    "created_at": "2016-03-15T08:48:40.592Z",
+    "coordinateProperties": {
+      "cas": [
+        96.71454,
+        96.47705000000002
+      ],
+      "image_keys": [
+        "76P0YUrlDD_lF6J7Od3yoA",
+        "Ap_8E0BwoAqqewhJaEbFyQ"
+      ]
+    },
+    "key": "cHBf9e8n0pG8O0ZVQHGFBQ",
+    "pano": false,
+    "user_key": "AGfe-07BEJX0-kxpu9J3rA"
+  },
+  "geometry": {
+    "type": "LineString",
+    "coordinates": [
+      [
+        16.43279,
+        7.246085
+      ],
+      [
+        16.432799,
+        7.246082
+      ]
+    ]
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile.json	(revision 34386)
@@ -0,0 +1,7 @@
+{
+  "about": "Mapillary and Mapping!",
+  "avatar": "https://d4vkkeqw582u.cloudfront.net/3f9f044b34b498ddfb9afbb6/profile.png",
+  "created_at": "2013-09-18T16:52:28.042Z",
+  "key": "2BJl04nvnfW1y2GNaj7x5w",
+  "username": "gyllen"
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile2.json
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile2.json	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/api/v3/responses/userProfile2.json	(revision 34386)
@@ -0,0 +1,7 @@
+{
+  "about": "Having a non-image avatar",
+  "avatar": "https://example.org",
+  "created_at": "2016-01-31T01:47:28.000+0500",
+  "key": "abcdefg1",
+  "username": "mapillary_userÄ2!"
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/dateTimeOnly.metadata.txt
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/dateTimeOnly.metadata.txt	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/dateTimeOnly.metadata.txt	(revision 34386)
@@ -0,0 +1,1 @@
+add Exif.Photo.DateTimeOriginal 2015:12:24 01:02:03
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/generateExifTaggedImages.sh
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/generateExifTaggedImages.sh	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/generateExifTaggedImages.sh	(revision 34386)
@@ -0,0 +1,10 @@
+#!/bin/bash
+rm latLonOnly.jpg
+rm dateTimeOnly.jpg
+rm gpsDirectionOnly.jpg
+cp -T untagged.jpg latLonOnly.jpg
+cp -T untagged.jpg dateTimeOnly.jpg
+cp -T untagged.jpg gpsDirectionOnly.jpg
+exiv2 -m latLonOnly.metadata.txt latLonOnly.jpg
+exiv2 -m dateTimeOnly.metadata.txt dateTimeOnly.jpg
+exiv2 -m gpsDirectionOnly.metadata.txt gpsDirectionOnly.jpg
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/gpsDirectionOnly.metadata.txt
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/gpsDirectionOnly.metadata.txt	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/gpsDirectionOnly.metadata.txt	(revision 34386)
@@ -0,0 +1,1 @@
+add Exif.GPSInfo.GPSImgDirection 4273/100
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/latLonOnly.metadata.txt
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/latLonOnly.metadata.txt	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/exifTestImages/latLonOnly.metadata.txt	(revision 34386)
@@ -0,0 +1,4 @@
+add Exif.GPSInfo.GPSLatitude 55/1 36/1 19/1
+add Exif.GPSInfo.GPSLatitudeRef N
+add Exif.GPSInfo.GPSLongitude 13/1 0/1 1/2
+add Exif.GPSInfo.GPSLongitudeRef E
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/data/preferences/preferences.xml
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/data/preferences/preferences.xml	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/data/preferences/preferences.xml	(revision 34386)
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<preferences xmlns="http://josm.openstreetmap.de/preferences-1.0">
+  <tag key='expert' value='true'/>
+  <tag key='language' value='en'/>
+</preferences>
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideAbstractImageTest.java	(revision 34386)
@@ -0,0 +1,37 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class StreetsideAbstractImageTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().platform();
+
+  @Ignore
+  @Test
+  public void testIsModified() {
+    StreetsideImage img = new StreetsideImage("key___________________", new LatLon(0, 0), 0);
+    assertFalse(img.isModified());
+    img.turn(1e-4);
+    img.stopMoving();
+    assertTrue(img.isModified());
+    img.turn(-1e-4);
+    img.stopMoving();
+    assertFalse(img.isModified());
+    img.move(1e-4, 1e-4);
+    img.stopMoving();
+    assertTrue(img.isModified());
+    img.move(-1e-4, -1e-4);
+    img.stopMoving();
+    assertFalse(img.isModified());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideDataTest.java	(revision 34386)
@@ -0,0 +1,159 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside;
+
+import static org.junit.Assert.assertEquals;
+
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
+
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Tests for {@link StreetsideData} class.
+ *
+ * @author nokutu
+ * @see StreetsideData
+ */
+public class StreetsideDataTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().platform();
+
+  private StreetsideData data;
+  private StreetsideImage img1;
+  private StreetsideImage img2;
+  private StreetsideImage img3;
+  private StreetsideImage img4;
+
+  /**
+   * Creates a sample {@link StreetsideData} objects, 4 {@link StreetsideImage}
+   * objects and a {@link StreetsideSequence} object.
+   */
+  @Before
+  public void setUp() {
+    img1 = new StreetsideImage("id1__________________", new LatLon(0.1, 0.1), 90);
+    img2 = new StreetsideImage("id2__________________", new LatLon(0.2, 0.2), 90);
+    img3 = new StreetsideImage("id3__________________", new LatLon(0.3, 0.3), 90);
+    img4 = new StreetsideImage("id4__________________", new LatLon(0.4, 0.4), 90);
+    final StreetsideSequence seq = new StreetsideSequence();
+
+    seq.add(Arrays.asList(img1, img2, img3, img4));
+
+    data = new StreetsideData();
+    data.addAll(new ConcurrentSkipListSet<>(seq.getImages()));
+  }
+
+  /**
+   * Tests the addition of new images. If a second image with the same key as
+   * another one in the database, the one that is being added should be ignored.
+   */
+  @Test
+  public void addTest() {
+    data = new StreetsideData();
+    assertEquals(0, data.getImages().size());
+    data.add(img1);
+    assertEquals(1, data.getImages().size());
+    data.add(img1);
+    assertEquals(1, data.getImages().size());
+    data.addAll(new ConcurrentSkipListSet<>(Arrays.asList(img2, img3)));
+    assertEquals(3, data.getImages().size());
+    data.addAll(new ConcurrentSkipListSet<>(Arrays.asList(img3, img4)));
+    assertEquals(4, data.getImages().size());
+  }
+
+  /**
+   * Test that the size is properly calculated.
+   */
+  @Test
+  public void sizeTest() {
+    assertEquals(4, data.getImages().size());
+    data.add(new StreetsideImage("id5__________________", new LatLon(0.1, 0.1), 90));
+    assertEquals(5, data.getImages().size());
+  }
+
+  /**
+   * Test the {@link StreetsideData#setHighlightedImage(StreetsideAbstractImage)}
+   * and {@link StreetsideData#getHighlightedImage()} methods.
+   */
+  @Test
+  public void highlighTest() {
+    data.setHighlightedImage(img1);
+    assertEquals(img1, data.getHighlightedImage());
+
+    data.setHighlightedImage(null);
+    assertEquals(null, data.getHighlightedImage());
+  }
+
+  /**
+   * Tests the selection of images.
+   */
+  @Ignore
+  @Test
+  public void selectTest() {
+    data.setSelectedImage(img1);
+    assertEquals(img1, data.getSelectedImage());
+
+    data.setSelectedImage(img4);
+    assertEquals(img4, data.getSelectedImage());
+
+    data.setSelectedImage(null);
+    assertEquals(null, data.getSelectedImage());
+  }
+
+  /**
+   * Tests the {@link StreetsideData#selectNext()} and
+   * {@link StreetsideData#selectPrevious()} methods.
+   */
+  @Ignore
+  @Test
+  public void nextAndPreviousTest() {
+    data.setSelectedImage(img1);
+
+    data.selectNext();
+    assertEquals(img2, data.getSelectedImage());
+    data.selectNext();
+    assertEquals(img3, data.getSelectedImage());
+    data.selectPrevious();
+    assertEquals(img2, data.getSelectedImage());
+
+    data.setSelectedImage(null);
+  }
+
+  @Ignore
+  @Test(expected=IllegalStateException.class)
+  public void nextOfNullImgTest() {
+    data.setSelectedImage(null);
+    data.selectNext();
+  }
+
+  @Ignore
+  @Test(expected=IllegalStateException.class)
+  public void previousOfNullImgTest() {
+    data.setSelectedImage(null);
+    data.selectPrevious();
+  }
+
+  /**
+   * Test the multiselection of images. When a new image is selected, the
+   * multiselected List should reset.
+   */
+  @Ignore
+  @Test
+  public void multiSelectTest() {
+    assertEquals(0, data.getMultiSelectedImages().size());
+    data.setSelectedImage(img1);
+    assertEquals(1, data.getMultiSelectedImages().size());
+    data.addMultiSelectedImage(img2);
+    assertEquals(2, data.getMultiSelectedImages().size());
+    data.setSelectedImage(img1);
+    assertEquals(1, data.getMultiSelectedImages().size());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideLayerTest.java	(revision 34386)
@@ -0,0 +1,84 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside;
+
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.imagery.ImageryInfo;
+import org.openstreetmap.josm.gui.layer.ImageryLayer;
+import org.openstreetmap.josm.gui.layer.Layer;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class StreetsideLayerTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().main().preferences().projection();
+
+  private static Layer getDummyLayer() {
+    return ImageryLayer.create(new ImageryInfo("dummy", "https://example.org"));
+  }
+
+  @Test
+  public void testGetIcon() {
+    assertNotNull(StreetsideLayer.getInstance().getIcon());
+  }
+
+  @Test
+  public void testIsMergable() {
+    assertFalse(StreetsideLayer.getInstance().isMergable(getDummyLayer()));
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testMergeFrom() {
+    StreetsideLayer.getInstance().mergeFrom(getDummyLayer());
+  }
+
+  @Test
+  public void testSetVisible() {
+    StreetsideLayer.getInstance().getData().add(new StreetsideImportedImage(CubemapUtils.IMPORTED_ID, new LatLon(0.0, 0.0), 0.0, new File("")));
+    StreetsideLayer.getInstance().getData().add(new StreetsideImportedImage(CubemapUtils.IMPORTED_ID, new LatLon(0.0, 0.0), 0.0, new File("")));
+    StreetsideImportedImage invisibleImage = new StreetsideImportedImage(CubemapUtils.IMPORTED_ID, new LatLon(0.0, 0.0), 0.0, new File(""));
+    invisibleImage.setVisible(false);
+    StreetsideLayer.getInstance().getData().add(invisibleImage);
+
+    StreetsideLayer.getInstance().setVisible(false);
+    for (StreetsideAbstractImage img : StreetsideLayer.getInstance().getData().getImages()) {
+      assertEquals(false, img.isVisible());
+    }
+
+
+    StreetsideLayer.getInstance().setVisible(true);
+    for (StreetsideAbstractImage img : StreetsideLayer.getInstance().getData().getImages()) {
+      assertEquals(true, img.isVisible());
+    }
+  }
+
+  @Test
+  public void testGetInfoComponent() {
+    Object comp = StreetsideLayer.getInstance().getInfoComponent();
+    assertTrue(comp instanceof String);
+    assertTrue(((String) comp).length() >= 9);
+  }
+
+  @Test
+  public void testClearInstance() {
+    StreetsideLayer.getInstance();
+    assertTrue(StreetsideLayer.hasInstance());
+    JOSMTestRules.cleanLayerEnvironment();
+    //assertFalse(StreetsideLayer.hasInstance());
+    StreetsideLayer.getInstance();
+    assertTrue(StreetsideLayer.hasInstance());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideSequenceTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideSequenceTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/StreetsideSequenceTest.java	(revision 34386)
@@ -0,0 +1,77 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.openstreetmap.josm.data.coor.LatLon;
+
+/**
+ * Tests for the {@link StreetsideSequence} class.
+ *
+ * @author nokutu
+ * @see StreetsideSequence
+ */
+public class StreetsideSequenceTest {
+
+  private final StreetsideImage img1 = new StreetsideImage("key1", new LatLon(0.1, 0.1), 90);
+  private final StreetsideImage img2 = new StreetsideImage("key2", new LatLon(0.2, 0.2), 90);
+  private final StreetsideImage img3 = new StreetsideImage("key3", new LatLon(0.3, 0.3), 90);
+  private final StreetsideImage img4 = new StreetsideImage("key4", new LatLon(0.4, 0.4), 90);
+  private final StreetsideImage imgWithoutSeq = new StreetsideImage("key5", new LatLon(0.5, 0.5), 90);
+  private final StreetsideSequence seq = new StreetsideSequence();
+
+  /**
+   * Creates 4 {@link StreetsideImage} objects and puts them in a
+   * {@link StreetsideSequence} object.
+   */
+  @Before
+  public void setUp() {
+    seq.add(Arrays.asList(img1, img2, img3, img4));
+  }
+
+  /**
+   * Tests the {@link StreetsideSequence#next(StreetsideAbstractImage)} and
+   * {@link StreetsideSequence#previous(StreetsideAbstractImage)}.
+   */
+  @Test
+  public void nextAndPreviousTest() {
+    assertEquals(img2, img1.next());
+    assertEquals(img1, img2.previous());
+    assertEquals(img3, img2.next());
+    assertEquals(img2, img3.previous());
+    assertEquals(img4, img3.next());
+    assertEquals(img3, img4.previous());
+
+
+    assertNull(img4.next());
+    assertNull(img1.previous());
+
+    assertNull(imgWithoutSeq.next());
+    assertNull(imgWithoutSeq.previous());
+
+    // Test IllegalArgumentException when asking for the next image of an image
+    // that is not in the sequence.
+    try {
+      seq.next(imgWithoutSeq);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertTrue(true);
+    }
+    // Test IllegalArgumentException when asking for the previous image of an
+    // image that is not in the sequence.
+    try {
+      seq.previous(imgWithoutSeq);
+      fail();
+    } catch (IllegalArgumentException e) {
+      assertTrue(true);
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/CachesTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/CachesTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/CachesTest.java	(revision 34386)
@@ -0,0 +1,14 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.cache;
+
+import org.junit.Test;
+
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
+
+public class CachesTest {
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(Caches.class);
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cache/StreetsideCacheTest.java	(revision 34386)
@@ -0,0 +1,37 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.cache;
+//License: GPL. For details, see LICENSE file.
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.openstreetmap.josm.plugins.streetside.cache.StreetsideCache.Type;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class StreetsideCacheTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().preferences();
+
+  @Test
+  public void test() {
+    StreetsideCache cache = new StreetsideCache("00000", Type.FULL_IMAGE);
+    assertNotNull(cache.getUrl());
+    assertNotNull(cache.getCacheKey());
+
+    assertFalse(cache.isObjectLoadable());
+
+    cache = new StreetsideCache("00000", Type.THUMBNAIL);
+    assertNotNull(cache.getCacheKey());
+    assertNotNull(cache.getUrl());
+
+    cache = new StreetsideCache(null, null);
+    assertNull(cache.getCacheKey());
+    assertNull(cache.getUrl());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilderTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilderTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapBuilderTest.java	(revision 34386)
@@ -0,0 +1,66 @@
+/**
+ *
+ */
+package org.openstreetmap.josm.plugins.streetside.cubemap;
+
+import static org.junit.Assert.fail;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * @author renerr18
+ *
+ */
+public class CubemapBuilderTest {
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @AfterClass
+  public static void tearDownAfterClass() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  /**
+   * Test method for {@link org.openstreetmap.josm.plugins.streetside.cubemap.CubemapBuilder#downloadCubemapImages(java.lang.String)}.
+   */
+  @Ignore
+  @Test
+  public final void testDownloadCubemapImages() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  /**
+   * Test method for {@link org.openstreetmap.josm.plugins.streetside.cubemap.CubemapBuilder#tileAdded(java.lang.String)}.
+   */
+  @Ignore
+  @Test
+  public final void testTileAdded() {
+    fail("Not yet implemented"); // TODO
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/CubemapUtilsTest.java	(revision 34386)
@@ -0,0 +1,60 @@
+package org.openstreetmap.josm.plugins.streetside.cubemap;
+
+import static org.junit.Assert.*;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class CubemapUtilsTest {
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+  }
+
+  @AfterClass
+  public static void tearDownAfterClass() throws Exception {
+  }
+
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  @Ignore
+  @Test
+  public final void testConvertDecimal2Quaternary() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  @Ignore
+  @Test
+  public final void testConvertQuaternary2Decimal() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  @Ignore
+  @Test
+  public final void testGetFaceNumberForCount() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  @Ignore
+  @Test
+  public final void testGetCount4FaceNumber() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  @Ignore
+  @Test
+  public final void testConvertDoubleCountNrto16TileNr() {
+    fail("Not yet implemented"); // TODO
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/GraphicsUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/GraphicsUtilsTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/GraphicsUtilsTest.java	(revision 34386)
@@ -0,0 +1,76 @@
+/**
+ *
+ */
+package org.openstreetmap.josm.plugins.streetside.cubemap;
+
+import static org.junit.Assert.*;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * @author renerr18
+ *
+ */
+public class GraphicsUtilsTest {
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @AfterClass
+  public static void tearDownAfterClass() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  /**
+   * @throws java.lang.Exception
+   */
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  /**
+   * Test method for {@link org.openstreetmap.josm.plugins.streetside.cubemap.GraphicsUtils#convertBufferedImage2JavaFXImage(java.awt.image.BufferedImage)}.
+   */
+  @Ignore
+  @Test
+  public final void testConvertBufferedImage2JavaFXImage() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  /**
+   * Test method for {@link org.openstreetmap.josm.plugins.streetside.cubemap.GraphicsUtils#buildMultiTiledCubemapFaceImage(java.awt.image.BufferedImage[])}.
+   */
+  @Ignore
+  @Test
+  public final void testBuildMultiTiledCubemapFaceImage() {
+    fail("Not yet implemented"); // TODO
+  }
+
+  /**
+   * Test method for {@link org.openstreetmap.josm.plugins.streetside.cubemap.GraphicsUtils#rotateImage(java.awt.image.BufferedImage)}.
+   */
+  @Ignore
+  @Test
+  public final void testRotateImage() {
+    fail("Not yet implemented"); // TODO
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/cubemap/TileDownloadingTaskTest.java	(revision 34386)
@@ -0,0 +1,36 @@
+package org.openstreetmap.josm.plugins.streetside.cubemap;
+
+import static org.junit.Assert.*;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class TileDownloadingTaskTest {
+
+  @BeforeClass
+  public static void setUpBeforeClass() throws Exception {
+  }
+
+  @AfterClass
+  public static void tearDownAfterClass() throws Exception {
+  }
+
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  @Ignore
+  @Test
+  public final void testCall() {
+    fail("Not yet implemented"); // TODO
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/ImageDisplayTest.java	(revision 34386)
@@ -0,0 +1,92 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.gui;
+
+import static org.junit.Assert.assertEquals;
+
+import java.awt.GraphicsEnvironment;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseWheelEvent;
+import java.awt.image.BufferedImage;
+
+import javax.swing.JFrame;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Tests {@link StreetsideImageDisplay}
+ */
+public class ImageDisplayTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().preferences();
+
+  private static final BufferedImage DUMMY_IMAGE = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
+
+  @Ignore
+  @Test
+  public void testImagePersistence() {
+    StreetsideImageDisplay display = new StreetsideImageDisplay();
+    display.setImage(DUMMY_IMAGE, null);
+    assertEquals(DUMMY_IMAGE, display.getImage());
+  }
+
+  /**
+   * This test does not check if the scroll events result in the correct changes in the {@link StreetsideImageDisplay},
+   * it only checks if the tested method runs through.
+   */
+  @Ignore
+  @Test
+  public void testMouseWheelMoved() {
+    if (GraphicsEnvironment.isHeadless()) {
+      return;
+    }
+    StreetsideImageDisplay display = new StreetsideImageDisplay();
+    final MouseWheelEvent dummyScroll = new MouseWheelEvent(display, 42, System.currentTimeMillis(), 0, 0, 0, 0, false, MouseWheelEvent.WHEEL_UNIT_SCROLL, 1, 3);
+    display.getMouseWheelListeners()[0].mouseWheelMoved(dummyScroll);
+
+    display.setImage(DUMMY_IMAGE, null);
+
+    display.getMouseWheelListeners()[0].mouseWheelMoved(dummyScroll);
+
+    // This is necessary to make the size of the component > 0. If you know a more elegant solution, feel free to change it.
+    JFrame frame = new JFrame();
+    frame.setSize(42, 42);
+    frame.getContentPane().add(display);
+    frame.pack();
+
+    display.getMouseWheelListeners()[0].mouseWheelMoved(dummyScroll);
+  }
+
+  /**
+   * This test does not check if the scroll events result in the correct changes in the {@link StreetsideImageDisplay},
+   * it only checks if the tested method runs through.
+   */
+  @Ignore
+  @Test
+  public void testMouseClicked() {
+    if (GraphicsEnvironment.isHeadless()) {
+      return;
+    }
+    for (int button = 1; button <= 3; button++) {
+      StreetsideImageDisplay display = new StreetsideImageDisplay();
+      final MouseEvent dummyClick = new MouseEvent(display, 42, System.currentTimeMillis(), 0, 0, 0, 1, false, button);
+      display.getMouseListeners()[0].mouseClicked(dummyClick);
+
+      display.setImage(DUMMY_IMAGE, null);
+
+      display.getMouseListeners()[0].mouseClicked(dummyClick);
+
+      // This is necessary to make the size of the component > 0. If you know a more elegant solution, feel free to change it.
+      JFrame frame = new JFrame();
+      frame.setSize(42, 42);
+      frame.getContentPane().add(display);
+      frame.pack();
+
+      display.getMouseListeners()[0].mouseClicked(dummyClick);
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/StreetsidePreferenceSettingTest.java	(revision 34386)
@@ -0,0 +1,144 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.gui;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.openstreetmap.josm.plugins.streetside.utils.TestUtil.getPrivateFieldValue;
+
+import java.awt.GraphicsEnvironment;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SpinnerNumberModel;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.plugins.streetside.io.download.StreetsideDownloader.DOWNLOAD_MODE;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.I18n;
+
+public class StreetsidePreferenceSettingTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().main();
+
+  @Ignore
+  @Test
+  public void testAddGui() {
+    if (GraphicsEnvironment.isHeadless()) {
+      return;
+    }
+    PreferenceTabbedPane tabs = new PreferenceTabbedPane();
+    tabs.buildGui();
+    int displayTabs = tabs.getDisplayPreference().getTabPane().getTabCount();
+    StreetsidePreferenceSetting setting = new StreetsidePreferenceSetting();
+    setting.addGui(tabs);
+    assertEquals(displayTabs + 1, tabs.getDisplayPreference().getTabPane().getTabCount());
+    assertEquals(tabs.getDisplayPreference(), setting.getTabPreferenceSetting(tabs));
+  }
+
+  @Ignore
+  @Test
+  public void testIsExpert() {
+    assertFalse(new StreetsidePreferenceSetting().isExpert());
+  }
+
+  @Ignore
+  @Test
+  public void testLoginLogout() {
+    if (GraphicsEnvironment.isHeadless()) {
+      return;
+    }
+    PreferenceTabbedPane tabs = new PreferenceTabbedPane();
+    tabs.buildGui();
+    StreetsidePreferenceSetting setting = new StreetsidePreferenceSetting();
+    setting.addGui(tabs);
+    setting.onLogout();
+
+    assertEquals(I18n.tr("Login"), ((JButton) getPrivateFieldValue(setting, "loginButton")).getText());
+    assertEquals(I18n.tr("You are currently not logged in."), ((JLabel) getPrivateFieldValue(setting, "loginLabel")).getText());
+    assertFalse(((JPanel) getPrivateFieldValue(setting, "loginPanel")).isAncestorOf(((JButton) getPrivateFieldValue(setting, "logoutButton"))));
+    assertTrue(((JPanel) getPrivateFieldValue(setting, "loginPanel")).isAncestorOf(((JButton) getPrivateFieldValue(setting, "loginButton"))));
+
+    String username = "TheStreetsideUsername";
+    setting.onLogin(username);
+
+    assertEquals(I18n.tr("Login"), ((JButton) getPrivateFieldValue(setting, "loginButton")).getText());
+    assertEquals(I18n.tr("You are logged in as ''{0}''.", username), ((JLabel) getPrivateFieldValue(setting, "loginLabel")).getText());
+    assertTrue(((JPanel) getPrivateFieldValue(setting, "loginPanel")).isAncestorOf(((JButton) getPrivateFieldValue(setting, "logoutButton"))));
+    assertFalse(((JPanel) getPrivateFieldValue(setting, "loginPanel")).isAncestorOf(((JButton) getPrivateFieldValue(setting, "loginButton"))));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Ignore
+  @Test
+  public void testOk() {
+    StreetsidePreferenceSetting settings = new StreetsidePreferenceSetting();
+
+    // Initialize the properties with some arbitrary value to make sure they are not unset
+    new StringProperty("streetside.display-hour", "default").put("arbitrary");
+    new StringProperty("streetside.format-24", "default").put("arbitrary");
+    new StringProperty("streetside.move-to-picture", "default").put("arbitrary");
+    new StringProperty("streetside.hover-enabled", "default").put("arbitrary");
+    new StringProperty("streetside.download-mode", "default").put("arbitrary");
+    new StringProperty("streetside.prefetch-image-count", "default").put("arbitrary");
+
+    // Test checkboxes
+    settings.ok();
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "displayHour"), "streetside.display-hour");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "format24"), "streetside.format-24");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "moveTo"), "streetside.move-to-picture");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "hoverEnabled"), "streetside.hover-enabled");
+    assertEquals(String.valueOf(((SpinnerNumberModel) getPrivateFieldValue(settings, "preFetchSize")).getNumber().intValue()), new StringProperty("streetside.prefetch-image-count", "default").get());
+
+    // Toggle state of the checkboxes
+    toggleCheckbox((JCheckBox) getPrivateFieldValue(settings, "displayHour"));
+    toggleCheckbox((JCheckBox) getPrivateFieldValue(settings, "format24"));
+    toggleCheckbox((JCheckBox) getPrivateFieldValue(settings, "moveTo"));
+    toggleCheckbox((JCheckBox) getPrivateFieldValue(settings, "hoverEnabled"));
+    ((SpinnerNumberModel) getPrivateFieldValue(settings, "preFetchSize")).setValue(73);
+
+    // Test the second state of the checkboxes
+    settings.ok();
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "displayHour"), "streetside.display-hour");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "format24"), "streetside.format-24");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "moveTo"), "streetside.move-to-picture");
+    assertPropertyMatchesCheckboxSelection((JCheckBox) getPrivateFieldValue(settings, "hoverEnabled"), "streetside.hover-enabled");
+    assertEquals(String.valueOf(((SpinnerNumberModel) getPrivateFieldValue(settings, "preFetchSize")).getNumber().intValue()), new StringProperty("streetside.prefetch-image-count", "default").get());
+
+    // Test combobox
+    for (int i = 0; i < ((JComboBox<String>) getPrivateFieldValue(settings, "downloadModeComboBox")).getItemCount(); i++) {
+      ((JComboBox<String>) getPrivateFieldValue(settings, "downloadModeComboBox")).setSelectedIndex(i);
+      settings.ok();
+      assertEquals(
+        new StringProperty("streetside.download-mode", "default").get(),
+        DOWNLOAD_MODE.fromLabel(
+          ((JComboBox<String>) getPrivateFieldValue(settings, "downloadModeComboBox")).getSelectedItem().toString()
+        ).getPrefId()
+      );
+    }
+  }
+
+  /**
+   * Checks, if a certain {@link BooleanProperty} (identified by the {@code propName} attribute) matches the selected-state of the given {@link JCheckBox}
+   * @param cb the {@link JCheckBox}, which should be checked against the {@link BooleanProperty}
+   * @param propName the name of the property against which the selected-state of the given {@link JCheckBox} should be checked
+   */
+  private static void assertPropertyMatchesCheckboxSelection(JCheckBox cb, String propName) {
+    assertEquals(cb.isSelected(), new BooleanProperty(propName, !cb.isSelected()).get());
+  }
+
+  private static void toggleCheckbox(JCheckBox jcb) {
+    jcb.setSelected(!jcb.isSelected());
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabelTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabelTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/gui/boilerplate/SelectableLabelTest.java	(revision 34386)
@@ -0,0 +1,19 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.gui.boilerplate;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public class SelectableLabelTest {
+  @Test
+  public void test() {
+    SelectableLabel l1 = new SelectableLabel();
+    assertFalse(l1.isEditable());
+    SelectableLabel l2 = new SelectableLabel("some text");
+    assertTrue(l2.getText().contains("some text"));
+    assertFalse(l2.isEditable());
+
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/history/StreetsideRecordTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/history/StreetsideRecordTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/history/StreetsideRecordTest.java	(revision 34386)
@@ -0,0 +1,238 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.history;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
+import org.openstreetmap.josm.plugins.streetside.history.commands.CommandJoin;
+import org.openstreetmap.josm.plugins.streetside.history.commands.CommandMove;
+import org.openstreetmap.josm.plugins.streetside.history.commands.CommandTurn;
+import org.openstreetmap.josm.plugins.streetside.history.commands.CommandUnjoin;
+import org.openstreetmap.josm.plugins.streetside.history.commands.StreetsideCommand;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Tests the command record system.
+ *
+ * @author nokutu
+ */
+public class StreetsideRecordTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().main().projection();
+
+  private StreetsideRecord record;
+  private StreetsideImage img1;
+  private StreetsideImage img2;
+  private StreetsideImage img3;
+
+  /**
+   * Creates a new {@link StreetsideRecord} object and 3 {@link StreetsideImage}
+   * objects.
+   */
+  @Before
+  public void setUp() {
+    record = new StreetsideRecord();
+    img1 = new StreetsideImage("key1__________________", new LatLon(0.1, 0.1), 0.1);
+    img2 = new StreetsideImage("key2__________________", new LatLon(0.2, 0.2), 0.2);
+    img3 = new StreetsideImage("key3__________________", new LatLon(0.3, 0.3), 0.3);
+    if (StreetsideLayer.hasInstance() && StreetsideLayer.getInstance().getData().getImages().size() >= 1) {
+      StreetsideLayer.getInstance().getData().getImages().clear();
+    }
+  }
+
+  /**
+   * Test commands in general.
+   */
+  @Test
+  public void commandTest() {
+    StreetsideCommand cmd12 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img2)),
+            0.1, 0.1);
+    StreetsideCommand cmd23 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img2, img3)),
+            0.1, 0.1);
+    StreetsideCommand cmd13 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img3)),
+            0.1, 0.1);
+    StreetsideCommand cmd1 = new CommandMove(
+            new ConcurrentSkipListSet<>(Collections.singletonList(img1)), 0.1, 0.1);
+    StreetsideCommand cmd31 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img3, img1)),
+            0.2, 0.2);
+    record.addCommand(cmd12);
+    record.addCommand(cmd23);
+
+    assertEquals(1, record.position);
+    assertEquals(2, record.commandList.size());
+
+    record.undo();
+
+    assertEquals(0, record.position);
+    assertEquals(2, record.commandList.size());
+
+    record.addCommand(cmd1);
+
+    assertEquals(1, record.position);
+
+    record.addCommand(cmd13);
+
+    assertEquals(2, record.position);
+    assertEquals(3, record.commandList.size());
+
+    record.undo();
+    record.redo();
+
+    assertEquals(2, record.position);
+    assertEquals(3, record.commandList.size());
+
+    record.addCommand(cmd31);
+
+    assertEquals(2, record.position);
+    assertEquals(3, record.commandList.size());
+
+    record.addCommand(cmd1);
+
+    assertEquals(3, record.position);
+    assertEquals(4, record.commandList.size());
+  }
+
+  /**
+   * Tests {@link CommandMove} class.
+   */
+  @Test
+  public void commandMoveTest() {
+    CommandMove cmd1 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img2)),
+            0.1, 0.1);
+    CommandMove cmd2 = new CommandMove(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img2)),
+            0.1, 0.1);
+
+    record.addCommand(cmd1);
+
+    assertEquals(0.1, img1.getMovingLatLon().lat(), 0.01);
+
+    record.undo();
+
+    assertEquals(0.0, img1.getMovingLatLon().lat(), 0.01);
+
+    record.redo();
+
+    assertEquals(0.1, img1.getMovingLatLon().lat(), 0.01);
+
+    record.addCommand(cmd2);
+    record.undo();
+
+    assertEquals(-0.1, img1.getMovingLatLon().lat(), 0.01);
+
+    record.redo();
+
+    assertEquals(0.1, img1.getMovingLatLon().lat(), 0.01);
+  }
+
+  /**
+   * Tests {@link CommandTurn} class.
+   */
+  @Test
+  public void commandTurnTest() {
+    CommandTurn cmd1 = new CommandTurn(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img2)),
+            0.2);
+    CommandTurn cmd2 = new CommandTurn(
+            new ConcurrentSkipListSet<>(Arrays.asList(img1, img2)),
+            0.1);
+
+    record.addCommand(cmd1);
+    record.undo();
+
+    assertEquals(-0.1, img1.getMovingHe(), 0.01);
+
+    record.redo();
+
+    assertEquals(0.1, img1.getMovingHe(), 0.01);
+
+    record.addCommand(cmd2);
+    record.undo();
+
+    assertEquals(-0.2, img1.getMovingHe(), 0.01);
+
+    record.redo();
+
+    assertEquals(0.1, img1.getMovingHe(), 0.01);
+  }
+
+  /**
+   * Tests {@link CommandJoin} class.
+   */
+  @Test
+  public void commandJoinClass() {
+    CommandJoin cmd1 = new CommandJoin(img1, img2);
+    CommandJoin cmd2 = new CommandJoin(img2, img3);
+
+    record.addCommand(cmd1);
+    assertEquals(2, img1.getSequence().getImages().size());
+    assertEquals(img2, img1.next());
+    record.undo();
+    assertEquals(1, img1.getSequence().getImages().size());
+    record.redo();
+    record.addCommand(cmd2);
+    assertEquals(3, img1.getSequence().getImages().size());
+    assertEquals(img3, img1.next().next());
+  }
+
+  @Test(expected=NullPointerException.class)
+  public void commandJoinNull1() {
+    new CommandJoin(img1, null);
+  }
+
+  @Test(expected=NullPointerException.class)
+  public void commandJoinNull2() {
+    new CommandJoin(null, img1);
+  }
+
+  /**
+   * Tests {@link CommandUnjoin} class.
+   */
+  @Test
+  public void commandUnjoinClass() {
+    CommandJoin join1 = new CommandJoin(img1, img2);
+    CommandJoin join2 = new CommandJoin(img2, img3);
+
+    CommandUnjoin cmd1 = new CommandUnjoin(
+            Arrays.asList(new StreetsideAbstractImage[]{img1, img2}));
+    CommandUnjoin cmd2 = new CommandUnjoin(
+            Arrays.asList(new StreetsideAbstractImage[]{img2, img3}));
+
+    record.addCommand(join1);
+    record.addCommand(join2);
+
+    record.addCommand(cmd1);
+    assertEquals(1, img1.getSequence().getImages().size());
+    record.undo();
+    assertEquals(3, img1.getSequence().getImages().size());
+    record.redo();
+    record.addCommand(cmd2);
+    assertEquals(1, img1.getSequence().getImages().size());
+    assertEquals(1, img2.getSequence().getImages().size());
+
+    try {
+      record.addCommand(new CommandUnjoin(Arrays.asList(new StreetsideAbstractImage[]{img1, img2, img3})));
+      fail();
+    } catch (IllegalArgumentException e) {
+      // Expected output.
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/io/download/SequenceDownloadRunnableTest.java	(revision 34386)
@@ -0,0 +1,80 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.io.download;
+
+import static org.junit.Assert.assertEquals;
+
+import java.lang.reflect.Field;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.function.Function;
+
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.plugins.streetside.StreetsideLayer;
+import org.openstreetmap.josm.plugins.streetside.utils.StreetsideProperties;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class SequenceDownloadRunnableTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().platform();
+
+  private static final Function<Bounds, URL> SEARCH_SEQUENCES_URL_GEN = b -> {
+    // TODO: modify and use Streetside URL @rrh
+    return SequenceDownloadRunnableTest.class.getResource("/api/v3/responses/searchSequences.json");
+  };
+  private Field urlGenField;
+
+  @AfterClass
+  public static void tearDown() {
+    MainApplication.getLayerManager().resetState();
+  }
+
+  @Test
+  @Ignore // TODO: fox!
+  public void testRun1() throws IllegalArgumentException, IllegalAccessException {
+    testNumberOfDecodedImages(4, SEARCH_SEQUENCES_URL_GEN, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
+  }
+
+  @Test
+  @Ignore // TODO: fox!
+  public void testRun2() throws IllegalArgumentException, IllegalAccessException {
+    testNumberOfDecodedImages(0, SEARCH_SEQUENCES_URL_GEN, new Bounds(0, 0, 0, 0));
+  }
+
+  @Test
+  @Ignore // TODO: fox!
+  public void testRun3() throws IllegalArgumentException, IllegalAccessException {
+    testNumberOfDecodedImages(0, b -> {
+      try { return new URL("https://streetside/nonexistentURL"); } catch (MalformedURLException e) { return null; }
+    }, new Bounds(0, 0, 0, 0));
+  }
+
+  @Test
+  @Ignore // TODO: fox!
+  public void testRun4() throws IllegalArgumentException, IllegalAccessException {
+    StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(true);
+    testNumberOfDecodedImages(4, SEARCH_SEQUENCES_URL_GEN, new Bounds(7.246497, 16.432955, 7.249027, 16.432976));
+  }
+
+  @Test
+  @Ignore // TODO: fox!
+  public void testRun5() throws IllegalArgumentException, IllegalAccessException {
+    StreetsideProperties.CUT_OFF_SEQUENCES_AT_BOUNDS.put(true);
+    testNumberOfDecodedImages(0, SEARCH_SEQUENCES_URL_GEN, new Bounds(0, 0, 0, 0));
+  }
+
+  private void testNumberOfDecodedImages(int expectedNumImgs, Function<Bounds, URL> urlGen, Bounds bounds)
+      throws IllegalArgumentException, IllegalAccessException {
+    SequenceDownloadRunnable r = new SequenceDownloadRunnable(StreetsideLayer.getInstance().getData(), bounds);
+    urlGenField.set(null, urlGen);
+    r.run();
+    assertEquals(expectedNumImgs, StreetsideLayer.getInstance().getData().getImages().size());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/ImageUtilTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/ImageUtilTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/ImageUtilTest.java	(revision 34386)
@@ -0,0 +1,32 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import java.awt.image.BufferedImage;
+
+import javax.swing.ImageIcon;
+
+import org.junit.Test;
+
+public class ImageUtilTest {
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(ImageUtil.class);
+  }
+
+  @Test
+  public void testScaleImageIcon() {
+    ImageIcon largeLandscape = new ImageIcon(new BufferedImage(72, 60, BufferedImage.TYPE_4BYTE_ABGR));
+    ImageIcon largePortrait = new ImageIcon(new BufferedImage(56, 88, BufferedImage.TYPE_4BYTE_ABGR));
+    ImageIcon smallLandscape = ImageUtil.scaleImageIcon(largeLandscape, 24);
+    ImageIcon smallPortrait = ImageUtil.scaleImageIcon(largePortrait, 22);
+
+    assertEquals(24, smallLandscape.getIconWidth());
+    assertEquals(20, smallLandscape.getIconHeight());
+
+    assertEquals(22, smallPortrait.getIconHeight());
+    assertEquals(14, smallPortrait.getIconWidth());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/JsonUtil.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/JsonUtil.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/JsonUtil.java	(revision 34386)
@@ -0,0 +1,18 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+public final class JsonUtil {
+  private JsonUtil() {
+    // Private constructor to avoid instantiation
+  }
+
+  public static JsonObject string2jsonObject(String s) {
+    return Json.createReader(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))).readObject();
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/PluginStateTest.java	(revision 34386)
@@ -0,0 +1,53 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import org.openstreetmap.josm.Main;
+
+/**
+ * Tests {@link PluginState} class.
+ *
+ * @author nokutu
+ * @see PluginState
+ */
+public class PluginStateTest {
+
+  /**
+   * Test the methods related to the download.
+   */
+  @Test
+  public void downloadTest() {
+    assertEquals(false, PluginState.isDownloading());
+    PluginState.startDownload();
+    assertEquals(true, PluginState.isDownloading());
+    PluginState.startDownload();
+    assertEquals(true, PluginState.isDownloading());
+    PluginState.finishDownload();
+    assertEquals(true, PluginState.isDownloading());
+    PluginState.finishDownload();
+    assertEquals(false, PluginState.isDownloading());
+  }
+
+  /**
+   * Tests the methods related to the upload.
+   */
+  @Test
+  public void uploadTest() {
+    Main.main = null;
+    assertEquals(false, PluginState.isUploading());
+    PluginState.addImagesToUpload(2);
+    assertEquals(2, PluginState.getImagesToUpload());
+    assertEquals(0, PluginState.getImagesUploaded());
+    assertEquals(true, PluginState.isUploading());
+    PluginState.imageUploaded();
+    assertEquals(1, PluginState.getImagesUploaded());
+    assertEquals(true, PluginState.isUploading());
+    PluginState.imageUploaded();
+    assertEquals(false, PluginState.isUploading());
+    assertEquals(2, PluginState.getImagesToUpload());
+    assertEquals(2, PluginState.getImagesUploaded());
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorSchemeTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorSchemeTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideColorSchemeTest.java	(revision 34386)
@@ -0,0 +1,20 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import javax.swing.JComponent;
+
+import org.junit.Test;
+
+public class StreetsideColorSchemeTest {
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(StreetsideColorScheme.class);
+  }
+
+  @Test
+  public void testStyleAsDefaultPanel() {
+    StreetsideColorScheme.styleAsDefaultPanel();
+    StreetsideColorScheme.styleAsDefaultPanel((JComponent[]) null);
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsidePropertiesTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsidePropertiesTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsidePropertiesTest.java	(revision 34386)
@@ -0,0 +1,20 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class StreetsidePropertiesTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules();
+
+  @Test
+  public void test() {
+    TestUtil.testUtilityClass(StreetsideProperties.class);
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideURLTest.java	(revision 34386)
@@ -0,0 +1,181 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class StreetsideURLTest {
+  // TODO: replace with Streetside URL @rrh
+  private static final String CLIENT_ID_QUERY_PART = "client_id=T1Fzd20xZjdtR0s1VDk5OFNIOXpYdzoxNDYyOGRkYzUyYTFiMzgz";
+
+  public static class APIv3 {
+
+    /*@Ignore
+	  @Test
+    public void testSearchDetections() {
+      assertUrlEquals(StreetsideURL.APIv3.searchDetections(null), "https://a.streetside.com/v3/detections", CLIENT_ID_QUERY_PART);
+    }
+
+    @Ignore
+    @Test
+    public void testSearchImages() {
+      assertUrlEquals(StreetsideURL.APIv3.searchImages(null), "https://a.streetside.com/v3/images", CLIENT_ID_QUERY_PART);
+    }
+
+    @Ignore
+    @Test
+    public void testSubmitChangeset() throws MalformedURLException {
+      assertEquals(
+        new URL("https://a.streetside.com/v3/changesets?" + CLIENT_ID_QUERY_PART),
+        StreetsideURL.APIv3.submitChangeset()
+      );
+    }*/
+  }
+
+
+	@Test
+    public void testParseNextFromHeaderValue() throws MalformedURLException {
+      String headerVal =
+        "<https://a.streetside.com/v3/sequences?page=1&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"first\", " +
+        "<https://a.streetside.com/v3/sequences?page=2&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"prev\", " +
+        "<https://a.streetside.com/v3/sequences?page=4&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2>; rel=\"next\"";
+      assertEquals(
+        new URL("https://a.streetside.com/v3/sequences?page=4&per_page=200&client_id=TG1sUUxGQlBiYWx2V05NM0pQNUVMQTo2NTU3NTBiNTk1NzM1Y2U2"),
+        StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal)
+      );
+    }
+
+    @Test
+    public void testParseNextFromHeaderValue2() throws MalformedURLException {
+      String headerVal =
+        "<https://urlFirst>; rel=\"first\", " +
+        "rel = \"next\" ; < ; , " +
+        "rel = \"next\" ; <https://urlNext> , " +
+        "<https://urlPrev>; rel=\"prev\"";
+      assertEquals(new URL("https://urlNext"), StreetsideURL.APIv3.parseNextFromLinkHeaderValue(headerVal));
+    }
+
+    @Test
+    public void testParseNextFromHeaderValueNull() {
+      assertEquals(null, StreetsideURL.APIv3.parseNextFromLinkHeaderValue(null));
+    }
+
+    @Test
+    public void testParseNextFromHeaderValueMalformed() {
+      assertEquals(null, StreetsideURL.APIv3.parseNextFromLinkHeaderValue("<###>; rel=\"next\", blub"));
+    }
+
+
+  /*public static class Cloudfront {
+    @Ignore
+	@Test
+    public void testThumbnail() {
+      assertUrlEquals(StreetsideURL.VirtualEarth.streetsideTile("arbitrary_key", true), "https://d1cuyjsrcm0gby.cloudfront.net/arbitrary_key/thumb-2048.jpg");
+      assertUrlEquals(StreetsideURL.VirtualEarth.streetsideTile("arbitrary_key2", false), "https://d1cuyjsrcm0gby.cloudfront.net/arbitrary_key2/thumb-320.jpg");
+    }
+  }*/
+
+  @Ignore
+  @Test
+  public void testBrowseImageURL() throws MalformedURLException {
+    assertEquals(
+        new URL("https://www.streetside.com/map/im/1234567890123456789012"),
+        StreetsideURL.MainWebsite.browseImage("1234567890123456789012")
+    );
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testIllegalBrowseImageURL() {
+    StreetsideURL.MainWebsite.browseImage(null);
+  }
+
+  @Ignore
+  @Test
+  public void testConnectURL() {
+    /*assertUrlEquals(
+        StreetsideURL.MainWebsite.connect("http://redirect-host/ä"),
+        "https://www.streetside.com/connect",
+        CLIENT_ID_QUERY_PART,
+        "scope=user%3Aread+public%3Aupload+public%3Awrite",
+        "response_type=token",
+        "redirect_uri=http%3A%2F%2Fredirect-host%2F%C3%A4"
+    );
+
+    assertUrlEquals(
+        StreetsideURL.MainWebsite.connect(null),
+        "https://www.streetside.com/connect",
+        CLIENT_ID_QUERY_PART,
+        "scope=user%3Aread+public%3Aupload+public%3Awrite",
+        "response_type=token"
+    );
+
+    assertUrlEquals(
+        StreetsideURL.MainWebsite.connect(""),
+        "https://www.streetside.com/connect",
+        CLIENT_ID_QUERY_PART,
+        "scope=user%3Aread+public%3Aupload+public%3Awrite",
+        "response_type=token"
+    );*/
+  }
+
+  @Ignore
+  @Test
+  public void testUploadSecretsURL() throws MalformedURLException {
+    /*assertEquals(
+        new URL("https://a.streetside.com/v2/me/uploads/secrets?"+CLIENT_ID_QUERY_PART),
+        StreetsideURL.uploadSecretsURL()
+    );*/
+  }
+
+  @Ignore
+  @Test
+  public void testUserURL() throws MalformedURLException {
+    /*assertEquals(
+        new URL("https://a.streetside.com/v3/me?"+CLIENT_ID_QUERY_PART),
+        StreetsideURL.APIv3.userURL()
+    );*/
+  }
+
+  @Test
+  public void testString2MalformedURL()
+      throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+    Method method = StreetsideURL.class.getDeclaredMethod("string2URL", String[].class);
+    method.setAccessible(true);
+    assertNull(method.invoke(null, new Object[]{new String[]{"malformed URL"}})); // this simply invokes string2URL("malformed URL")
+    assertNull(method.invoke(null, new Object[]{null})); // invokes string2URL(null)
+  }
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(StreetsideURL.class);
+    TestUtil.testUtilityClass(StreetsideURL.APIv3.class);
+    TestUtil.testUtilityClass(StreetsideURL.VirtualEarth.class);
+    TestUtil.testUtilityClass(StreetsideURL.MainWebsite.class);
+  }
+
+  private static void assertUrlEquals(URL actualUrl, String expectedBaseUrl, String... expectedParams) {
+    final String actualUrlString = actualUrl.toString();
+    assertEquals(expectedBaseUrl, actualUrlString.contains("?") ? actualUrlString.substring(0, actualUrlString.indexOf('?')) : actualUrlString);
+    String[] actualParams = actualUrl.getQuery() == null ? new String[0] : actualUrl.getQuery().split("&");
+    assertEquals(expectedParams.length, actualParams.length);
+    for (int exIndex = 0; exIndex < expectedParams.length; exIndex++) {
+      boolean parameterIsPresent = false;
+      for (int acIndex = 0; !parameterIsPresent && acIndex < actualParams.length; acIndex++) {
+        parameterIsPresent |= actualParams[acIndex].equals(expectedParams[exIndex]);
+      }
+      assertTrue(
+          expectedParams[exIndex] + " was expected in the query string of " + actualUrl.toString() + " but wasn't there.",
+          parameterIsPresent
+      );
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/StreetsideUtilsTest.java	(revision 34386)
@@ -0,0 +1,43 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.commons.imaging.common.RationalNumber;
+import org.apache.commons.imaging.formats.tiff.constants.GpsTagConstants;
+import org.junit.Test;
+
+/**
+ * Tests the static methods of the class {@link StreetsideUtils}
+ *
+ * @see StreetsideUtils
+ * @author nokutu
+ *
+ */
+public class StreetsideUtilsTest {
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(StreetsideUtils.class);
+  }
+
+  /**
+   * Test {@link StreetsideUtils#degMinSecToDouble(RationalNumber[], String)}
+   * method.
+   */
+  @Test
+  public void degMinSecToDoubleTest() {
+    RationalNumber[] num = new RationalNumber[3];
+    num[0] = new RationalNumber(1, 1);
+    num[1] = new RationalNumber(0, 1);
+    num[2] = new RationalNumber(0, 1);
+    String ref = GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_NORTH;
+    assertEquals(1, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
+    ref = GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF_VALUE_SOUTH;
+    assertEquals(-1, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
+    num[0] = new RationalNumber(180, 1);
+    assertEquals(-180, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
+    num[0] = new RationalNumber(190, 1);
+    assertEquals(170, StreetsideUtils.degMinSecToDouble(num, ref), 0.01);
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/TestUtil.java	(revision 34386)
@@ -0,0 +1,97 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.awt.GraphicsEnvironment;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.logging.Level;
+
+import org.junit.runners.model.InitializationError;
+
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtil {
+
+  private TestUtil() {
+    // Prevent instantiation
+  }
+
+  public static Field getAccessibleField(Class<?> clazz, String fieldName) {
+    try {
+      Field result = clazz.getDeclaredField(fieldName);
+      result.setAccessible(true);
+      Field modifiers = Field.class.getDeclaredField("modifiers");
+      modifiers.setAccessible(true);
+      modifiers.setInt(result, modifiers.getInt(result) & ~Modifier.FINAL);
+      return result;
+    } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+      fail(e.getLocalizedMessage());
+    }
+    return null;
+  }
+
+  /**
+   * Helper method for obtaining the value of a private field
+   * @param object the object of which you want the private field
+   * @param name the name of the private field
+   * @return the current value that field has
+   */
+  public static Object getPrivateFieldValue(Object object, String name) {
+    try {
+      return getAccessibleField(object.getClass(), name).get(object);
+    } catch (IllegalAccessException | SecurityException e) {
+      fail(e.getLocalizedMessage());
+    }
+    return null;
+  }
+
+  /**
+   * This method tests utility classes for common coding standards (exactly one constructor that's private,
+   * only static methods, …) and fails the current test if one of those standards is not met.
+   * This is inspired by <a href="https://stackoverflow.com/a/10872497">an answer on StackOverflow.com</a> .
+   * @param c the class under test
+   */
+  public static void testUtilityClass(final Class<?> c) {
+    try {
+      // class must be final
+      assertTrue(Modifier.isFinal(c.getModifiers()));
+      // with exactly one constructor
+      assertEquals(1, c.getDeclaredConstructors().length);
+      final Constructor<?> constructor = c.getDeclaredConstructors()[0];
+      // constructor has to be private
+      assertTrue(!constructor.isAccessible() && Modifier.isPrivate(constructor.getModifiers()));
+      constructor.setAccessible(true);
+      // Call private constructor for code coverage
+      constructor.newInstance();
+      for (Method m : c.getMethods()) {
+        // Check if all methods are static
+        assertTrue(m.getDeclaringClass() != c || Modifier.isStatic(m.getModifiers()));
+      }
+    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+      fail(e.getLocalizedMessage());
+    }
+  }
+
+  public static class StreetsideTestRules extends JOSMTestRules {
+    @Override
+    protected void before() throws InitializationError, ReflectiveOperationException {
+      Logging.getLogger().setFilter(record -> record.getLevel().intValue() >= Level.WARNING.intValue() || record.getSourceClassName().startsWith("org.openstreetmap.josm.plugins.streetside"));
+      Utils.updateSystemProperty("java.util.logging.SimpleFormatter.format", "%1$tF %1$tT.%1$tL %2$s %4$s: %5$s%6$s%n");
+      final String isHeadless = Boolean.toString(GraphicsEnvironment.isHeadless());
+      super.before();
+      System.setProperty("java.awt.headless", isHeadless);
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonDecoderTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonDecoderTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonDecoderTest.java	(revision 34386)
@@ -0,0 +1,35 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils.api;
+
+import static org.junit.Assert.assertNull;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.junit.Test;
+
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
+
+public class JsonDecoderTest {
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(JsonDecoder.class);
+  }
+
+  @Test
+  public void testDecodeDoublePair() {
+    assertNull(JsonDecoder.decodeDoublePair(null));
+  }
+
+  static void assertDecodesToNull(Function<JsonObject, ?> function, String...parts) {
+    assertNull(function.apply(
+      Json.createReader(new ByteArrayInputStream(String.join(" ", parts).getBytes(StandardCharsets.UTF_8))).readObject()
+    ));
+  }
+
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonImageDetailsDecoderTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonImageDetailsDecoderTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonImageDetailsDecoderTest.java	(revision 34386)
@@ -0,0 +1,101 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils.api;
+
+import static org.junit.Assert.assertEquals;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.StreetsideAbstractImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideData;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImportedImage;
+import org.openstreetmap.josm.plugins.streetside.cubemap.CubemapUtils;
+import org.openstreetmap.josm.plugins.streetside.utils.JsonUtil;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil.StreetsideTestRules;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+public class JsonImageDetailsDecoderTest {
+
+  @Rule
+  public JOSMTestRules rules = new StreetsideTestRules().platform().preferences();
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(JsonImageDetailsDecoder.class);
+  }
+
+  @Ignore
+  @Test
+  public void testDecodeImageInfos() {
+    JsonObject searchImagesResponse = Json.createReader(
+      JsonImageDetailsDecoderTest.class.getResourceAsStream("/api/v3/responses/searchImages.json")
+    ).readObject();
+    StreetsideData data = new StreetsideDataMock();
+    StreetsideImage img1 = new StreetsideImage("_yA5uXuSNugmsK5VucU6Bg", new LatLon(0, 0), 0);
+    StreetsideImage img2 = new StreetsideImage("nmF-Wq4EvVTgAUmBicSCCg", new LatLon(0, 0), 0);
+    StreetsideImage img3 = new StreetsideImage("arbitrary_key", new LatLon(0, 0), 0);
+    StreetsideAbstractImage img4 = new StreetsideImportedImage(CubemapUtils.IMPORTED_ID, new LatLon(0, 0), 0, null);
+    img4.setHe(0);
+    data.add(img1);
+    data.add(img2);
+    data.add(img3);
+    data.add(img4);
+    JsonImageDetailsDecoder.decodeImageInfos(searchImagesResponse, data);
+    assertEquals(1_491_803_490_334L, img1.getHe()); // 2017-04-10T05:51:30.334Z
+    assertEquals(1_491_803_486_853L, img2.getHe()); // 2017-04-10T05:51:26.853Z
+    assertEquals(0L, img3.getHe());
+    assertEquals(0L, img4.getHe());
+  }
+
+  @Test
+  public void testInvalidImageInfos() {
+    StreetsideDataMock data = new StreetsideDataMock();
+    JsonImageDetailsDecoder.decodeImageInfos(null, data);
+    JsonImageDetailsDecoder.decodeImageInfos(JsonUtil.string2jsonObject("{}"), null);
+    JsonImageDetailsDecoder.decodeImageInfos(JsonUtil.string2jsonObject("{}"), data);
+    JsonImageDetailsDecoder.decodeImageInfos(JsonUtil.string2jsonObject("{\"type\":\"FeatureCollection\", \"features\":0}"), data);
+    JsonImageDetailsDecoder.decodeImageInfos(JsonUtil.string2jsonObject("{\"type\":\"FeatureCollection\", \"features\":[0, null]}"), data);
+    assertEquals(0, data.getNumImageRerievals());
+  }
+
+  @Test
+  public void testInvalidImageInfo() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+    Method decodeImageInfo = JsonImageDetailsDecoder.class.getDeclaredMethod("decodeImageInfo", JsonObject.class, StreetsideData.class);
+    StreetsideDataMock data = new StreetsideDataMock();
+    decodeImageInfo.setAccessible(true);
+    decodeImageInfo.invoke(null, null, data);
+    decodeImageInfo.invoke(null, JsonUtil.string2jsonObject("{}"), null);
+    decodeImageInfo.invoke(null, JsonUtil.string2jsonObject("{\"properties\":null}"), data);
+    decodeImageInfo.invoke(null, JsonUtil.string2jsonObject("{\"properties\":{}}"), data);
+    decodeImageInfo.invoke(null, JsonUtil.string2jsonObject("{\"properties\":{\"key\":\"arbitrary_key\"}}"), data);
+    assertEquals(0, data.getNumImageRerievals());
+  }
+
+  private static class StreetsideDataMock extends StreetsideData {
+    private int imageRetrievals;
+
+    /**
+     * Returns how often the method {@link #getImages()} has been accessed for this instance.
+     * @return
+     */
+    public int getNumImageRerievals() {
+      return imageRetrievals;
+    }
+
+    @Override
+    public Set<StreetsideAbstractImage> getImages() {
+      imageRetrievals++;
+      return super.getImages();
+    }
+  }
+}
Index: /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonSequencesDecoderTest.java
===================================================================
--- /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonSequencesDecoderTest.java	(revision 34386)
+++ /applications/editors/josm/plugins/MicrosoftStreetside/test/unit/org/openstreetmap/josm/plugins/streetside/utils/api/JsonSequencesDecoderTest.java	(revision 34386)
@@ -0,0 +1,219 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.plugins.streetside.utils.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.openstreetmap.josm.plugins.streetside.utils.api.JsonDecoderTest.assertDecodesToNull;
+
+import java.io.ByteArrayInputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.function.Function;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.plugins.streetside.StreetsideImage;
+import org.openstreetmap.josm.plugins.streetside.StreetsideSequence;
+import org.openstreetmap.josm.plugins.streetside.utils.JsonUtil;
+import org.openstreetmap.josm.plugins.streetside.utils.TestUtil;
+
+public class JsonSequencesDecoderTest {
+
+
+  @Ignore
+  @Test
+  public void testDecodeSequences() {
+    Collection<StreetsideSequence> exampleSequences = JsonDecoder.decodeFeatureCollection(
+      Json.createReader(this.getClass().getResourceAsStream("/api/v3/responses/searchSequences.json")).readObject(),
+      JsonSequencesDecoder::decodeSequence
+    );
+    assertNotNull(exampleSequences);
+    assertEquals(1, exampleSequences.size());
+    StreetsideSequence seq = exampleSequences.iterator().next();
+    assertEquals(4, seq.getImages().size());
+
+    assertEquals("LwrHXqFRN_pszCopTKHF_Q", ((StreetsideImage) seq.getImages().get(0)).getId());
+    assertEquals("Aufjv2hdCKwg9LySWWVSwg", ((StreetsideImage) seq.getImages().get(1)).getId());
+    assertEquals("QEVZ1tp-PmrwtqhSwdW9fQ", ((StreetsideImage) seq.getImages().get(2)).getId());
+    assertEquals("G_SIwxNcioYeutZuA8Rurw", ((StreetsideImage) seq.getImages().get(3)).getId());
+
+    assertEquals(323.0319999999999, seq.getImages().get(0).getHe(), 1e-10);
+    assertEquals(320.8918, seq.getImages().get(1).getHe(), 1e-10);
+    assertEquals(333.62239999999997, seq.getImages().get(2).getHe(), 1e-10);
+    assertEquals(329.94820000000004, seq.getImages().get(3).getHe(), 1e-10);
+
+    assertEquals(new LatLon(7.246497, 16.432958),  seq.getImages().get(0).getLatLon());
+    assertEquals(new LatLon(7.246567, 16.432955),  seq.getImages().get(1).getLatLon());
+    assertEquals(new LatLon(7.248372, 16.432971),  seq.getImages().get(2).getLatLon());
+    assertEquals(new LatLon(7.249027, 16.432976),  seq.getImages().get(3).getLatLon());
+
+    assertEquals(1_457_963_093_860L, seq.getCd()); // 2016-03-14T13:44:53.860 UTC
+  }
+
+  @Test
+  public void testDecodeSequencesInvalid() {
+    // null input
+    assertEquals(0, JsonDecoder.decodeFeatureCollection(null, JsonSequencesDecoder::decodeSequence).size());
+    // empty object
+    assertNumberOfDecodedSequences(0, "{}");
+    // object without type=FeatureCollection
+    assertNumberOfDecodedSequences(0, "{\"features\": []}");
+    // object with wrong value for the type attribute
+    assertNumberOfDecodedSequences(0, "{\"type\": \"SomethingArbitrary\", \"features\": []}");
+    // object without features-array
+    assertNumberOfDecodedSequences(0, "{\"type\": \"FeatureCollection\"}");
+    // object with a value for the features-key, but wrong type (in this case string instead of Array)
+    assertNumberOfDecodedSequences(0, "{\"type\": \"FeatureCollection\", \"features\": \"notAnArray\"}");
+  }
+
+  @Test
+  public void testDecodeSequencesWithArbitraryObjectAsFeature() {
+    assertNumberOfDecodedSequences(0, "{\"type\": \"FeatureCollection\", \"features\": [{}]}");
+  }
+
+  private static void assertNumberOfDecodedSequences(int expectedNumberOfSequences, String jsonString) {
+    assertEquals(
+      expectedNumberOfSequences,
+      JsonDecoder.decodeFeatureCollection(
+        Json.createReader(new ByteArrayInputStream(jsonString.getBytes(StandardCharsets.UTF_8))).readObject(),
+        JsonSequencesDecoder::decodeSequence
+      ).size()
+    );
+  }
+
+  @Ignore
+  @Test
+  public void testDecodeSequence() {
+    StreetsideSequence exampleSequence = JsonSequencesDecoder.decodeSequence(
+      Json.createReader(this.getClass().getResourceAsStream("/api/v3/responses/sequence.json")).readObject()
+    );
+    assertEquals("cHBf9e8n0pG8O0ZVQHGFBQ", exampleSequence.getId());
+    assertEquals(1_457_963_077_206L, exampleSequence.getCd()); // 2016-03-14T13:44:37.206 UTC
+    assertEquals(2, exampleSequence.getImages().size());
+
+    assertEquals(
+      new StreetsideImage("76P0YUrlDD_lF6J7Od3yoA", new LatLon(16.43279, 7.246085), 96.71454),
+      exampleSequence.getImages().get(0)
+    );
+    assertEquals(
+      new StreetsideImage("Ap_8E0BwoAqqewhJaEbFyQ", new LatLon(16.432799, 7.246082), 96.47705000000002),
+      exampleSequence.getImages().get(1)
+    );
+  }
+
+  @Test
+  public void testDecodeSequenceInvalid() {
+    // null input
+    assertNull(JsonSequencesDecoder.decodeSequence(null));
+    // `properties` key is not set
+    assertDecodesToNull(JsonSequencesDecoder::decodeSequence, "{\"type\": \"Feature\"}");
+    // value for the key `key` in the properties is missing
+    assertDecodesToNull(JsonSequencesDecoder::decodeSequence,
+      "{\"type\": \"Feature\", \"properties\": {}}"
+    );
+    // value for the key `user_key` in the properties is missing
+    assertDecodesToNull(
+      JsonSequencesDecoder::decodeSequence,
+      "{\"type\": \"Feature\", \"properties\": {\"key\": \"someKey\"}}"
+    );
+    // value for the key `captured_at` in the properties is missing
+    assertDecodesToNull(
+      JsonSequencesDecoder::decodeSequence,
+      "{\"type\": \"Feature\", \"properties\": {\"key\": \"someKey\", \"user_key\": \"arbitraryUserKey\"}}"
+    );
+    // the date in `captured_at` has an unrecognized format
+    assertDecodesToNull(
+      JsonSequencesDecoder::decodeSequence,
+      "{\"type\": \"Feature\", \"properties\": {\"key\": \"someKey\", \"captured_at\": \"unrecognizedDateFormat\"}}"
+    );
+    // the `image_key` array and the `cas` array contain unexpected values (in this case `null`)
+    assertDecodesToNull(
+      JsonSequencesDecoder::decodeSequence,
+      "{\"type\": \"Feature\", \"properties\": {\"key\": \"someKey\", \"user_key\": \"arbitraryUserKey\",",
+      "\"captured_at\": \"1970-01-01T00:00:00.000Z\",",
+      "\"coordinateProperties\": {\"cas\": [null, null, null, null, 1.0, 1.0, 1.0],",
+      "\"image_keys\": [null, null, \"key\", \"key\", null, null, \"key\"]}},",
+      "\"geometry\": {\"type\": \"LineString\", \"coordinates\": [null, [1,1], null, [1,1], null, [1,1], null]}}"
+    );
+  }
+
+  /**
+   * Checks if an empty array is returned, if <code>null</code> is supplied to the method as the array.
+   */
+  @Test
+  public void testDecodeJsonArray()
+      throws NoSuchMethodException, SecurityException, IllegalAccessException,
+      IllegalArgumentException, InvocationTargetException {
+    Method method = JsonSequencesDecoder.class.getDeclaredMethod("decodeJsonArray", JsonArray.class, Function.class, Class.class);
+    method.setAccessible(true);
+    assertEquals(
+      0,
+      ((String[]) method.invoke(null, null, (Function<JsonValue, String>) val -> null, String.class)).length
+    );
+  }
+
+  @Test
+  public void testDecodeCoordinateProperty() throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+    Method decodeCoordinateProperty = JsonSequencesDecoder.class.getDeclaredMethod(
+      "decodeCoordinateProperty",
+      JsonObject.class,
+      String.class,
+      Function.class,
+      Class.class
+    );
+    decodeCoordinateProperty.setAccessible(true);
+
+    assertEquals(
+      0,
+      ((String[]) decodeCoordinateProperty.invoke(
+        null, null, "key", (Function<JsonValue, String>) val -> "string", String.class
+      )).length
+    );
+
+    assertEquals(
+      0,
+      ((String[]) decodeCoordinateProperty.invoke(
+        null, JsonUtil.string2jsonObject("{\"coordinateProperties\":{\"key\":0}}"), "key", (Function<JsonValue, String>) val -> "string", String.class
+      )).length
+    );
+  }
+
+  @Test
+  public void testDecodeLatLons()
+      throws NoSuchMethodException, SecurityException, IllegalAccessException,
+      IllegalArgumentException, InvocationTargetException {
+    Method decodeLatLons = JsonSequencesDecoder.class.getDeclaredMethod("decodeLatLons", JsonObject.class);
+    decodeLatLons.setAccessible(true);
+
+    assertEquals(0, ((LatLon[]) decodeLatLons.invoke(null, (JsonObject) null)).length);
+    assertEquals(0, ((LatLon[]) decodeLatLons.invoke(null, JsonUtil.string2jsonObject("{\"coordinates\":0}"))).length);
+    assertEquals(0, ((LatLon[]) decodeLatLons.invoke(null, JsonUtil.string2jsonObject("{\"coordinates\":[]}"))).length);
+
+    assertEquals(0, ((LatLon[]) decodeLatLons.invoke(null, Json.createReader(new ByteArrayInputStream(
+      "{\"type\": \"Feature\", \"coordinates\": []}".getBytes(StandardCharsets.UTF_8)
+    )).readObject())).length);
+
+    LatLon[] example = (LatLon[]) decodeLatLons.invoke(null, Json.createReader(new ByteArrayInputStream(
+      "{\"type\": \"LineString\", \"coordinates\": [ [1,2,3], [\"a\", 2], [1, \"b\"] ]}".getBytes(StandardCharsets.UTF_8)
+    )).readObject());
+    assertEquals(3, example.length);
+    assertNull(example[0]);
+    assertNull(example[1]);
+    assertNull(example[2]);
+  }
+
+  @Test
+  public void testUtilityClass() {
+    TestUtil.testUtilityClass(JsonSequencesDecoder.class);
+  }
+
+}
