From 2215e734ac2f7df48eb90a3fd57cf16447d5fe87 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jan=20Bedna=C5=99=C3=ADk?= <jan.bednarik@gmail.com>
Date: Sun, 21 Oct 2018 15:56:18 +0200
Subject: [PATCH] Update of old report creates historical revision

---
 README.md                                     |  54 ++--
 openlobby/core/api/mutations.py               |  17 +-
 openlobby/core/api/types.py                   |   1 -
 openlobby/settings.py                         |   3 +
 .../snapshots/snap_test_update_report.py      | 296 ++++++++++++++++--
 tests/mutations/test_update_report.py         | 197 +++++++-----
 tests/utils.py                                |   8 +
 7 files changed, 432 insertions(+), 144 deletions(-)

diff --git a/README.md b/README.md
index eb60e95..e96068b 100644
--- a/README.md
+++ b/README.md
@@ -1,20 +1,18 @@
 # Open Lobby Server
 
-Open Lobby is register of lobby meetings. It's being developed for and tested
-on [Czech Pirate Party](https://www.pirati.cz) but later it may be used by
+Open Lobby is register of lobby meetings. It's being developed for and tested 
+on [Czech Pirate Party](https://www.pirati.cz) but later it may be used by 
 any party, organization, agency, ...
 
-_Open Lobby is in early beta version now. Not for production use._
-
-This is core of the register - server with [GraphQL API](http://graphql.org).
-Over API are connected application interfaces. Default web application is
-available at
+This is core of the register - server with [GraphQL API](http://graphql.org). 
+Over API are connected application interfaces. Default web application is 
+available at 
 [openlobby/openlobby-app](https://github.com/openlobby/openlobby-app).
 
-Register is built on top of
-[Elasticsearch](https://www.elastic.co/products/elasticsearch). For now it's
-intended for search in Czech language with custom Czech text analyzer. There is
-prepared Elasticsearch Docker container with Czech support at
+Register is built on top of 
+[Elasticsearch](https://www.elastic.co/products/elasticsearch). For now it's 
+intended for search in Czech language with custom Czech text analyzer. There is 
+prepared Elasticsearch Docker container with Czech support at 
 [openlobby/openlobby-es-czech](https://github.com/openlobby/openlobby-es-czech).
 
 ## Configuration
@@ -29,24 +27,26 @@ Configuration is done by environment variables:
  - `REDIRECT_URI` - redirect URI used in OpenID Connect authentication (default: `http://localhost:8010/login-redirect`)
      - put there address where you run server, but keep there `/login-redirect`
      - this is the Redirect URI for static client registration at OpenID Provider
+ - `FREE_EDIT_MINUTES` - edit will save historical revision after this time 
+   since last edit (or publishing) of report (default: `60`)
 
 ### Login shortcuts aka preregistered OpenID Clients
 
-Some OpenID Providers does not allow dynamic client registration. You can still
-use them. Register client with `REDIRECT_URI` and save client's credentials into
-database. You can do it in admin interface running at `/admin`. It's standard
+Some OpenID Providers does not allow dynamic client registration. You can still 
+use them. Register client with `REDIRECT_URI` and save client's credentials into 
+database. You can do it in admin interface running at `/admin`. It's standard 
 Django admin (create superuser for yourself like `./manage.py createsuperuser`).
 
 ## Docker
 
-Docker image is at Docker Hub
-[openlobby/openlobby-server](https://hub.docker.com/r/openlobby/openlobby-server/).
-It exposes server on port 8010. You should provide it environment variables for
+Docker image is at Docker Hub 
+[openlobby/openlobby-server](https://hub.docker.com/r/openlobby/openlobby-server/). 
+It exposes server on port 8010. You should provide it environment variables for 
 configuration (at least `SECRET_KEY`).
 
 ## Demo
 
-Demo of Open Lobby with instructions is in repository
+Demo of Open Lobby with instructions is in repository 
 [openlobby/demo](https://github.com/openlobby/demo).
 
 ## Local run and development
@@ -55,13 +55,13 @@ Demo of Open Lobby with instructions is in repository
 
 You need to have Python 3 installed.
 
-Run PostgreSQL database on `localhost:5432` with user `db`, password `db` and
-database `openlobby`. You can provide different address in environment variable
+Run PostgreSQL database on `localhost:5432` with user `db`, password `db` and 
+database `openlobby`. You can provide different address in environment variable 
 `DATABASE_DSN`.
 
-Run Elasticsearch server
+Run Elasticsearch server 
 [openlobby/openlobby-es-czech](https://github.com/openlobby/openlobby-es-czech) 
-on `http://localhost:9200`. You can provide different address in environment
+on `http://localhost:9200`. You can provide different address in environment 
 variable `ELASTICSEARCH_DSN`.
 
 ### Local run
@@ -74,7 +74,7 @@ Clone this repository and run:
 4. `make migrate` - runs database migrations and rebuilds Elasticsearch index
 5. `make run` - runs development server on port `8010`
 
-Now you can use GraphQL API endpoint and GraphiQL web interface at
+Now you can use GraphQL API endpoint and GraphiQL web interface at 
 `http://localhost:8010/graphql`
 
 Next time you can just do steps 2 and 5.
@@ -83,14 +83,14 @@ Next time you can just do steps 2 and 5.
 
 Run: `pytest`
 
-For full test suite run you have to provide OpenID Provider issuer URL which
-allows client registration. For example you can run Keycloak sever locally:
+For full test suite run you have to provide OpenID Provider issuer URL which 
+allows client registration. For example you can run Keycloak sever locally: 
 `docker run -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=pass -p 8080:8080 --rm jboss/keycloak`
 
-Login into Keycloak admin console `http://localhost:8080/auth/admin/`
+Login into Keycloak admin console `http://localhost:8080/auth/admin/` 
 (as admin/pass) and go to Realm Settings -> Client Registration -> Client 
 Registration Policies -> Trusted Hosts. There add `localhost` to "Trusted 
-Hosts", turn off "Host Sending Client Registration Request Must Match" and save
+Hosts", turn off "Host Sending Client Registration Request Must Match" and save 
 it.
 
 Now run: `pytest --issuer=http://localhost:8080/auth/realms/master`
diff --git a/openlobby/core/api/mutations.py b/openlobby/core/api/mutations.py
index fa4e3a2..8eb7543 100644
--- a/openlobby/core/api/mutations.py
+++ b/openlobby/core/api/mutations.py
@@ -1,4 +1,5 @@
 import arrow
+from django.conf import settings
 import graphene
 from graphene import relay
 from graphene.types.datetime import DateTime
@@ -178,11 +179,21 @@ class UpdateReport(relay.ClientIDMutation):
         if is_draft and not report.is_draft:
             raise Exception("You cannot update published Report with draft.")
 
-        # TODO updating published report older than like a hour should create
-        # new revision in history of report
+        free_edit_deadline = arrow.utcnow().shift(minutes=-settings.FREE_EDIT_MINUTES)
+        if (
+            not (is_draft or report.is_draft)
+            and free_edit_deadline.datetime > report.edited
+        ):
+            original_id = report.id
+            # save historical revision copy
+            report.id = None
+            report.superseded_by_id = original_id
+            report.save()
+            # restore id so this can be updated
+            report.id = original_id
+            report.superseded_by_id = None
 
         report.edited = arrow.utcnow().datetime
-
         if is_draft or report.is_draft:
             report.published = report.edited
 
diff --git a/openlobby/core/api/types.py b/openlobby/core/api/types.py
index de692b8..a3d9966 100644
--- a/openlobby/core/api/types.py
+++ b/openlobby/core/api/types.py
@@ -1,4 +1,3 @@
-from django.db.models import Count, Q
 from elasticsearch import NotFoundError
 import graphene
 from graphene import relay
diff --git a/openlobby/settings.py b/openlobby/settings.py
index 1b08799..ff9786d 100644
--- a/openlobby/settings.py
+++ b/openlobby/settings.py
@@ -150,3 +150,6 @@ SITE_NAME = os.environ.get("SITE_NAME", "Open Lobby")
 
 # redirect URI used in OpenID authentication
 REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:8010/login-redirect")
+
+# edit will save historical revision after this time since last edit
+FREE_EDIT_MINUTES = os.environ.get("FREE_EDIT_MINUTES", 60)
diff --git a/tests/mutations/snapshots/snap_test_update_report.py b/tests/mutations/snapshots/snap_test_update_report.py
index 77a4e76..3c88475 100644
--- a/tests/mutations/snapshots/snap_test_update_report.py
+++ b/tests/mutations/snapshots/snap_test_update_report.py
@@ -58,22 +58,22 @@ snapshots["test_update_draft_with_draft 1"] = {
                 "author": {
                     "extra": '{"movies": 1}',
                     "firstName": "Winston",
-                    "id": "QXV0aG9yOjE=",
+                    "id": "QXV0aG9yOjQy",
                     "lastName": "Wolfe",
                     "totalReports": 0,
                 },
-                "body": "I visited Tesla factory and talked with Elon Musk.",
+                "body": "Rewrited",
                 "date": "2018-03-03 00:00:00+00:00",
-                "edited": "2018-03-08 12:00:00+00:00",
+                "edited": "2018-01-02 05:50:00+00:00",
                 "extra": None,
-                "id": "UmVwb3J0OjE=",
+                "id": "UmVwb3J0OjY2Ng==",
                 "isDraft": True,
-                "otherParticipants": "Elon Musk",
-                "ourParticipants": "me",
-                "providedBenefit": "nothing",
-                "published": "2018-03-08 12:00:00+00:00",
-                "receivedBenefit": "Tesla Model S",
-                "title": "Free Tesla",
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
+                "published": "2018-01-02 05:50:00+00:00",
+                "receivedBenefit": "cake",
+                "title": "New title",
             }
         }
     }
@@ -86,22 +86,22 @@ snapshots["test_update_draft_with_published 1"] = {
                 "author": {
                     "extra": '{"movies": 1}',
                     "firstName": "Winston",
-                    "id": "QXV0aG9yOjE=",
+                    "id": "QXV0aG9yOjQy",
                     "lastName": "Wolfe",
                     "totalReports": 1,
                 },
-                "body": "I visited Tesla factory and talked with Elon Musk.",
+                "body": "Rewrited",
                 "date": "2018-03-03 00:00:00+00:00",
-                "edited": "2018-03-08 12:00:00+00:00",
+                "edited": "2018-01-02 05:50:00+00:00",
                 "extra": None,
-                "id": "UmVwb3J0OjE=",
+                "id": "UmVwb3J0OjY2Ng==",
                 "isDraft": False,
-                "otherParticipants": "Elon Musk",
-                "ourParticipants": "me",
-                "providedBenefit": "nothing",
-                "published": "2018-03-08 12:00:00+00:00",
-                "receivedBenefit": "Tesla Model S",
-                "title": "Free Tesla",
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
+                "published": "2018-01-02 05:50:00+00:00",
+                "receivedBenefit": "cake",
+                "title": "New title",
             }
         }
     }
@@ -114,22 +114,22 @@ snapshots["test_update_published_with_published 1"] = {
                 "author": {
                     "extra": '{"movies": 1}',
                     "firstName": "Winston",
-                    "id": "QXV0aG9yOjE=",
+                    "id": "QXV0aG9yOjQy",
                     "lastName": "Wolfe",
                     "totalReports": 1,
                 },
-                "body": "I visited Tesla factory and talked with Elon Musk.",
+                "body": "Rewrited",
                 "date": "2018-03-03 00:00:00+00:00",
-                "edited": "2018-03-08 12:00:00+00:00",
+                "edited": "2018-01-02 05:50:00+00:00",
                 "extra": None,
-                "id": "UmVwb3J0OjE=",
+                "id": "UmVwb3J0OjY2Ng==",
                 "isDraft": False,
-                "otherParticipants": "Elon Musk",
-                "ourParticipants": "me",
-                "providedBenefit": "nothing",
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
                 "published": "2018-01-02 00:00:00+00:00",
-                "receivedBenefit": "Tesla Model S",
-                "title": "Free Tesla",
+                "receivedBenefit": "cake",
+                "title": "New title",
             }
         }
     }
@@ -142,15 +142,15 @@ snapshots["test_input_sanitization 1"] = {
                 "author": {
                     "extra": '{"movies": 1}',
                     "firstName": "Winston",
-                    "id": "QXV0aG9yOjE=",
+                    "id": "QXV0aG9yOjQy",
                     "lastName": "Wolfe",
                     "totalReports": 1,
                 },
                 "body": "some link in body",
                 "date": "2018-03-03 00:00:00+00:00",
-                "edited": "2018-03-08 12:00:00+00:00",
+                "edited": "2018-01-02 05:50:00+00:00",
                 "extra": None,
-                "id": "UmVwb3J0OjE=",
+                "id": "UmVwb3J0OjY2Ng==",
                 "isDraft": False,
                 "otherParticipants": "you!",
                 "ourParticipants": "me, myself",
@@ -162,3 +162,235 @@ snapshots["test_input_sanitization 1"] = {
         }
     }
 }
+
+snapshots["test_update_draft_with_draft__late_edit 1"] = {
+    "data": {
+        "updateReport": {
+            "report": {
+                "author": {
+                    "extra": '{"movies": 1}',
+                    "firstName": "Winston",
+                    "id": "QXV0aG9yOjQy",
+                    "lastName": "Wolfe",
+                    "totalReports": 0,
+                },
+                "body": "Rewrited",
+                "date": "2018-03-03 00:00:00+00:00",
+                "edited": "2018-01-02 06:10:00+00:00",
+                "extra": None,
+                "id": "UmVwb3J0OjY2Ng==",
+                "isDraft": True,
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
+                "published": "2018-01-02 06:10:00+00:00",
+                "receivedBenefit": "cake",
+                "title": "New title",
+            }
+        }
+    }
+}
+
+snapshots["test_update_draft_with_published__late_edit 1"] = {
+    "data": {
+        "updateReport": {
+            "report": {
+                "author": {
+                    "extra": '{"movies": 1}',
+                    "firstName": "Winston",
+                    "id": "QXV0aG9yOjQy",
+                    "lastName": "Wolfe",
+                    "totalReports": 1,
+                },
+                "body": "Rewrited",
+                "date": "2018-03-03 00:00:00+00:00",
+                "edited": "2018-01-02 06:10:00+00:00",
+                "extra": None,
+                "id": "UmVwb3J0OjY2Ng==",
+                "isDraft": False,
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
+                "published": "2018-01-02 06:10:00+00:00",
+                "receivedBenefit": "cake",
+                "title": "New title",
+            }
+        }
+    }
+}
+
+snapshots["test_update_draft_with_draft 2"] = [
+    {
+        "author_id": 42,
+        "body": "Rewrited",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T05:50:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": True,
+        "other_participants": "grandchilds",
+        "our_participants": "kids",
+        "provided_benefit": "water",
+        "published": "2018-01-02T05:50:00+00:00",
+        "received_benefit": "cake",
+        "superseded_by_id": None,
+        "title": "New title",
+    }
+]
+
+snapshots["test_update_draft_with_draft__late_edit 2"] = [
+    {
+        "author_id": 42,
+        "body": "Rewrited",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T06:10:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": True,
+        "other_participants": "grandchilds",
+        "our_participants": "kids",
+        "provided_benefit": "water",
+        "published": "2018-01-02T06:10:00+00:00",
+        "received_benefit": "cake",
+        "superseded_by_id": None,
+        "title": "New title",
+    }
+]
+
+snapshots["test_update_draft_with_published 2"] = [
+    {
+        "author_id": 42,
+        "body": "Rewrited",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T05:50:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": False,
+        "other_participants": "grandchilds",
+        "our_participants": "kids",
+        "provided_benefit": "water",
+        "published": "2018-01-02T05:50:00+00:00",
+        "received_benefit": "cake",
+        "superseded_by_id": None,
+        "title": "New title",
+    }
+]
+
+snapshots["test_update_draft_with_published__late_edit 2"] = [
+    {
+        "author_id": 42,
+        "body": "Rewrited",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T06:10:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": False,
+        "other_participants": "grandchilds",
+        "our_participants": "kids",
+        "provided_benefit": "water",
+        "published": "2018-01-02T06:10:00+00:00",
+        "received_benefit": "cake",
+        "superseded_by_id": None,
+        "title": "New title",
+    }
+]
+
+snapshots["test_update_published_with_published 2"] = [
+    {
+        "author_id": 42,
+        "body": "Rewrited",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T05:50:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": False,
+        "other_participants": "grandchilds",
+        "our_participants": "kids",
+        "provided_benefit": "water",
+        "published": "2018-01-02T00:00:00+00:00",
+        "received_benefit": "cake",
+        "superseded_by_id": None,
+        "title": "New title",
+    }
+]
+
+snapshots["test_update_published_with_published__late_edit 1"] = {
+    "data": {
+        "updateReport": {
+            "report": {
+                "author": {
+                    "extra": '{"movies": 1}',
+                    "firstName": "Winston",
+                    "id": "QXV0aG9yOjQy",
+                    "lastName": "Wolfe",
+                    "totalReports": 1,
+                },
+                "body": "Rewrited",
+                "date": "2018-03-03 00:00:00+00:00",
+                "edited": "2018-01-02 06:10:00+00:00",
+                "extra": None,
+                "id": "UmVwb3J0OjY2Ng==",
+                "isDraft": False,
+                "otherParticipants": "grandchilds",
+                "ourParticipants": "kids",
+                "providedBenefit": "water",
+                "published": "2018-01-02 00:00:00+00:00",
+                "receivedBenefit": "cake",
+                "title": "New title",
+            }
+        }
+    }
+}
+
+snapshots["test_update_published_with_published__late_edit 2"] = {
+    "author_id": 42,
+    "body": "Rewrited",
+    "date": "2018-03-03T00:00:00+00:00",
+    "edited": "2018-01-02T06:10:00+00:00",
+    "extra": None,
+    "id": 666,
+    "is_draft": False,
+    "other_participants": "grandchilds",
+    "our_participants": "kids",
+    "provided_benefit": "water",
+    "published": "2018-01-02T00:00:00+00:00",
+    "received_benefit": "cake",
+    "superseded_by_id": None,
+    "title": "New title",
+}
+
+snapshots["test_input_sanitization 2"] = [
+    {
+        "author_id": 42,
+        "body": "some link in body",
+        "date": "2018-03-03T00:00:00+00:00",
+        "edited": "2018-01-02T05:50:00+00:00",
+        "extra": None,
+        "id": 666,
+        "is_draft": False,
+        "other_participants": "you!",
+        "our_participants": "me, myself",
+        "provided_benefit": "tea",
+        "published": "2018-01-02T00:00:00+00:00",
+        "received_benefit": "coffee",
+        "superseded_by_id": None,
+        "title": "No tags",
+    }
+]
+
+snapshots["test_update_published_with_published__late_edit 3"] = {
+    "author_id": 42,
+    "body": "Previous body.",
+    "date": "2018-01-01T00:00:00+00:00",
+    "edited": "2018-01-02T05:00:00+00:00",
+    "extra": None,
+    "id": "__STRIPPED__",
+    "is_draft": False,
+    "other_participants": "grandma",
+    "our_participants": "grandpa",
+    "provided_benefit": "old tea",
+    "published": "2018-01-02T00:00:00+00:00",
+    "received_benefit": "old coffee",
+    "superseded_by_id": 666,
+    "title": "Original",
+}
diff --git a/tests/mutations/test_update_report.py b/tests/mutations/test_update_report.py
index d68f238..88a92e0 100644
--- a/tests/mutations/test_update_report.py
+++ b/tests/mutations/test_update_report.py
@@ -3,10 +3,9 @@ import arrow
 from graphql_relay import to_global_id
 from unittest.mock import patch
 
-from openlobby.core.models import User, Report
+from openlobby.core.models import Report
 
-from ..dummy import prepare_report
-from ..utils import call_api
+from ..utils import dates_to_iso, strip_value
 
 
 pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures("django_es")]
@@ -40,126 +39,162 @@ mutation updateReport ($input: UpdateReportInput!) {
 }
 """
 
-published = arrow.get(2018, 1, 2)
-edited = arrow.get(2018, 3, 8, 12)
+original_edited = arrow.get(2018, 1, 2, 5)
+edited = original_edited.shift(minutes=50)
+late_edited = original_edited.shift(minutes=70)
 
-date = arrow.get(2018, 3, 3)
-title = "Free Tesla"
-body = "I visited Tesla factory and talked with Elon Musk."
-received_benefit = "Tesla Model S"
-provided_benefit = "nothing"
-our_participants = "me"
-other_participants = "Elon Musk"
 
+@pytest.fixture
+def original_report(author_fix, report_factory):
+    return report_factory(
+        id=666,
+        author=author_fix,
+        date=arrow.get(2018, 1, 1).datetime,
+        published=arrow.get(2018, 1, 2).datetime,
+        edited=original_edited.datetime,
+        title="Original",
+        body="Previous body.",
+        received_benefit="old coffee",
+        provided_benefit="old tea",
+        our_participants="grandpa",
+        other_participants="grandma",
+    )
 
-def get_input(is_draft=False, id=1):
+
+@pytest.fixture
+def original_report_draft(original_report):
+    original_report.is_draft = True
+    original_report.save()
+    return original_report
+
+
+def prepare_input(is_draft=False, id=1):
     return {
         "id": to_global_id("Report", id),
-        "title": title,
-        "body": body,
-        "receivedBenefit": received_benefit,
-        "providedBenefit": provided_benefit,
-        "ourParticipants": our_participants,
-        "otherParticipants": other_participants,
-        "date": date.isoformat(),
+        "title": "New title",
+        "body": "Rewrited",
+        "receivedBenefit": "cake",
+        "providedBenefit": "water",
+        "ourParticipants": "kids",
+        "otherParticipants": "grandchilds",
+        "date": arrow.get(2018, 3, 3).isoformat(),
         "isDraft": is_draft,
     }
 
 
-def assert_report(is_draft, published, edited):
-    report = Report.objects.get(id=1)
-    assert report.author_id == 1
-    assert report.date == date.datetime
-    assert report.published == published.datetime
-    assert report.edited == edited.datetime
-    assert report.title == title
-    assert report.body == body
-    assert report.received_benefit == received_benefit
-    assert report.provided_benefit == provided_benefit
-    assert report.our_participants == our_participants
-    assert report.other_participants == other_participants
-    assert report.extra is None
-    assert report.is_draft is is_draft
-
-
-def test_unauthorized(client, snapshot):
-    prepare_report()
-    input = get_input()
-    response = call_api(client, query, input)
+def test_unauthorized(call_api, snapshot, original_report):
+    input = prepare_input(id=original_report.id)
+    response = call_api(query, input)
     snapshot.assert_match(response)
 
 
-def test_not_author(client, snapshot):
-    prepare_report()
-    User.objects.create(id=2, username="hacker")
-    input = get_input()
-    response = call_api(client, query, input, "hacker")
+def test_not_author(call_api, snapshot, original_report, user):
+    input = prepare_input(id=original_report.id)
+    response = call_api(query, input, user)
     snapshot.assert_match(response)
 
 
-def test_report_does_not_exist(client, snapshot):
-    prepare_report()
-    input = get_input(id=666)
-    response = call_api(client, query, input, "wolf")
+def test_report_does_not_exist(call_api, snapshot, author_fix):
+    input = prepare_input(id=789)
+    response = call_api(query, input, author_fix)
     snapshot.assert_match(response)
 
 
-def test_update_published_with_draft(client, snapshot):
-    prepare_report()
-    input = get_input(is_draft=True)
-    response = call_api(client, query, input, "wolf")
+def test_update_published_with_draft(call_api, snapshot, original_report):
+    input = prepare_input(id=original_report.id, is_draft=True)
+    response = call_api(query, input, original_report.author)
     snapshot.assert_match(response)
 
 
-def test_update_draft_with_draft(client, snapshot):
-    prepare_report(is_draft=True)
-    input = get_input(is_draft=True)
+def test_update_draft_with_draft(call_api, snapshot, original_report_draft):
+    input = prepare_input(id=original_report_draft.id, is_draft=True)
+
     with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=edited):
-        response = call_api(client, query, input, "wolf")
+        response = call_api(query, input, original_report_draft.author)
+
     snapshot.assert_match(response)
-    assert_report(True, edited, edited)
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
+
+
+def test_update_draft_with_draft__late_edit(call_api, snapshot, original_report_draft):
+    input = prepare_input(id=original_report_draft.id, is_draft=True)
 
+    with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=late_edited):
+        response = call_api(query, input, original_report_draft.author)
+
+    snapshot.assert_match(response)
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
+
+
+def test_update_draft_with_published(call_api, snapshot, original_report_draft):
+    input = prepare_input(id=original_report_draft.id)
 
-def test_update_draft_with_published(client, snapshot):
-    prepare_report(is_draft=True)
-    input = get_input()
     with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=edited):
-        response = call_api(client, query, input, "wolf")
+        response = call_api(query, input, original_report_draft.author)
+
     snapshot.assert_match(response)
-    assert_report(False, edited, edited)
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
+
 
+def test_update_draft_with_published__late_edit(
+    call_api, snapshot, original_report_draft
+):
+    input = prepare_input(id=original_report_draft.id)
+
+    with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=late_edited):
+        response = call_api(query, input, original_report_draft.author)
+
+    snapshot.assert_match(response)
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
+
+
+def test_update_published_with_published(call_api, snapshot, original_report):
+    input = prepare_input(id=original_report.id)
 
-def test_update_published_with_published(client, snapshot):
-    prepare_report()
-    input = get_input()
     with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=edited):
-        response = call_api(client, query, input, "wolf")
+        response = call_api(query, input, original_report.author)
+
     snapshot.assert_match(response)
-    assert_report(False, published, edited)
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
 
 
-def test_input_sanitization(client, snapshot):
-    prepare_report()
+def test_update_published_with_published__late_edit(
+    call_api, snapshot, original_report
+):
+    input = prepare_input(id=original_report.id)
+
+    with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=late_edited):
+        response = call_api(query, input, original_report.author)
+
+    snapshot.assert_match(response)
+    assert Report.objects.count() == 2
+    updated = Report.objects.filter(id=original_report.id).values()[0]
+    original = Report.objects.filter(superseded_by_id=original_report.id).values()[0]
+    strip_value(original, "id")
+    snapshot.assert_match(dates_to_iso(updated))
+    snapshot.assert_match(dates_to_iso(original))
+
+
+def test_input_sanitization(call_api, snapshot, original_report):
     input = {
-        "id": to_global_id("Report", 1),
+        "id": to_global_id("Report", original_report.id),
         "title": "<s>No</s> tags",
         "body": 'some <a href="http://foo">link</a> <br>in body',
         "receivedBenefit": "<b>coffee</b>",
         "providedBenefit": "<li>tea",
         "ourParticipants": "me, <u>myself</u>",
         "otherParticipants": "<strong>you!</strong>",
-        "date": date.isoformat(),
+        "date": arrow.get(2018, 3, 3).isoformat(),
     }
 
     with patch("openlobby.core.api.mutations.arrow.utcnow", return_value=edited):
-        response = call_api(client, query, input, "wolf")
+        response = call_api(query, input, original_report.author)
 
     snapshot.assert_match(response)
-
-    report = Report.objects.get()
-    assert report.title == "No tags"
-    assert report.body == "some link in body"
-    assert report.received_benefit == "coffee"
-    assert report.provided_benefit == "tea"
-    assert report.our_participants == "me, myself"
-    assert report.other_participants == "you!"
+    reports = list(map(dates_to_iso, Report.objects.all().values()))
+    snapshot.assert_match(reports)
diff --git a/tests/utils.py b/tests/utils.py
index e836d9d..f214c05 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,3 +1,4 @@
+from datetime import datetime
 import json
 
 from openlobby.core.auth import create_access_token
@@ -26,3 +27,10 @@ def strip_value(data, *path):
         return value
     else:
         return strip_value(value, *path[1:])
+
+
+def dates_to_iso(data):
+    for key, val in data.items():
+        if isinstance(val, datetime):
+            data[key] = val.isoformat()
+    return data
-- 
GitLab