Index: ow/tests/models/test_root.py
===================================================================
--- ow/tests/models/test_root.py	(revision 42baca45356aff1b03ad74e6c467053f46e60834)
+++ ow/tests/models/test_root.py	(revision 0dedfbefae135284808c7dd8c29c69933d1779f2)
@@ -2,4 +2,5 @@
 from unittest.mock import Mock
 from datetime import datetime, timedelta, timezone
+from decimal import Decimal
 
 import pytest
@@ -175,2 +176,19 @@
         john.add_workout(workout)
         assert root.sports_json == json.dumps(["cycling", "running"])
+
+    def test_get_workout_by_hash(self, root, john):
+        # non existant hash
+        found = root.get_workout_by_hash('non-existant-hash')
+        assert found is None
+        # existing workout
+        workout = root[str(john.uid)]['1']
+        found = root.get_workout_by_hash(workout.hashed)
+        assert found == workout
+        # a workout that was not added to the system
+        workout = Workout(
+            start_time=datetime.now(timezone.utc),
+            duration=timedelta(seconds=3600),
+            distance=Decimal(30)
+        )
+        found = root.get_workout_by_hash(workout.hashed)
+        assert found is None
Index: ow/tests/models/test_workout.py
===================================================================
--- ow/tests/models/test_workout.py	(revision 42baca45356aff1b03ad74e6c467053f46e60834)
+++ ow/tests/models/test_workout.py	(revision 0dedfbefae135284808c7dd8c29c69933d1779f2)
@@ -143,4 +143,33 @@
         workout.distance = 44.44444444
         assert workout.rounded_distance == 44.44
+
+    def test_hashed(self, root):
+        # first test a workout that is attached to a user
+        workout = root['john']['1']
+        assert workout.hashed == (
+            str(workout.owner.uid) +
+            workout.start.strftime('%Y%m%d%H%M%S') +
+            str(workout.duration.seconds) +
+            str(workout.distance)
+        )
+        # now a workout that is not (no owner info)
+        workout = Workout(
+            start_time=datetime.now(timezone.utc),
+            duration=timedelta(seconds=3600),
+            distance=Decimal(30)
+        )
+        assert workout.hashed == (
+            workout.start.strftime('%Y%m%d%H%M%S') +
+            str(workout.duration.seconds) +
+            str(workout.distance)
+        )
+        # now an empty workout...
+        workout = Workout()
+        with pytest.raises(AttributeError):
+            assert workout.hashed == (
+                workout.start.strftime('%Y%m%d%H%M%S') +
+                str(workout.duration.seconds) +
+                str(workout.distance)
+            )
 
     def test_trimmed_notes(self):
Index: ow/tests/views/test_workout.py
===================================================================
--- ow/tests/views/test_workout.py	(revision 42baca45356aff1b03ad74e6c467053f46e60834)
+++ ow/tests/views/test_workout.py	(revision 0dedfbefae135284808c7dd8c29c69933d1779f2)
@@ -182,4 +182,5 @@
         assert len(response['form'].form.errors) == 0
         assert isinstance(response['form'].form.schema, UploadedWorkoutSchema)
+        assert response['duplicate'] is None
 
     def test_add_workout_post_invalid(self, dummy_request):
@@ -196,4 +197,5 @@
         # Only one required field in this case, the tracking file
         assert len(response['form'].form.errors) == 1
+        assert response['duplicate'] is None
 
     def test_add_workout_post_invalid_bytes(self, dummy_request):
@@ -214,4 +216,5 @@
         # Only one required field in this case, the tracking file
         assert len(response['form'].form.errors) == 1
+        assert response['duplicate'] is None
 
     @pytest.mark.parametrize('filename', gpx_filenames)
@@ -234,4 +237,55 @@
         assert response.location.endswith('/2/')
         assert len(request.root['john'].workouts()) == 2
+        self.close_uploaded_file(uploaded_file)
+
+    def test_add_workout_post_duplicate(self, dummy_request):
+        """
+        POST request, first add a workout uploading one of the sample
+        tracking files, then try to upload it again.
+        """
+        # first, upload the workout
+        filename = self.gpx_filenames[0]
+        request = dummy_request
+        uploaded_file = self.open_uploaded_file(filename)
+        filestorage = self.create_filestorage(uploaded_file)
+        user = request.root['john']
+        request.method = 'POST'
+        request.POST = MultiDict({
+            'tracking_file': filestorage,
+            'submit': True,
+            })
+        assert len(request.root['john'].workouts()) == 1
+        response = workout_views.add_workout(user, request)
+        assert isinstance(response, HTTPFound)
+        assert response.location.endswith('/2/')
+        assert len(request.root['john'].workouts()) == 2
+        self.close_uploaded_file(uploaded_file)
+        # now, try to upload it again
+        uploaded_file = self.open_uploaded_file(filename)
+        filestorage = self.create_filestorage(uploaded_file)
+        user = request.root['john']
+        request.method = 'POST'
+        request.POST = MultiDict({
+            'tracking_file': filestorage,
+            'submit': True,
+            })
+        response = workout_views.add_workout(user, request)
+        assert response['duplicate'] == request.root['john']['2']
+        assert len(request.root['john'].workouts()) == 2
+        self.close_uploaded_file(uploaded_file)
+        # finally, override the duplicate prevention code and save it
+        uploaded_file = self.open_uploaded_file(filename)
+        filestorage = self.create_filestorage(uploaded_file)
+        user = request.root['john']
+        request.method = 'POST'
+        request.POST = MultiDict({
+            'tracking_file': filestorage,
+            'allow_duplicates': 'on',
+            'submit': True,
+            })
+        response = workout_views.add_workout(user, request)
+        assert isinstance(response, HTTPFound)
+        assert response.location.endswith('/3/')
+        assert len(request.root['john'].workouts()) == 3
         self.close_uploaded_file(uploaded_file)
 
