diff --git a/openlobby/core/api/mutations.py b/openlobby/core/api/mutations.py
index afd86b293c9ffcf5815005da509b0445100de2b8..e0e1c2bb9201c84e4f6c213ce5003d53ce3e9e70 100644
--- a/openlobby/core/api/mutations.py
+++ b/openlobby/core/api/mutations.py
@@ -1,3 +1,4 @@
+import arrow
 import graphene
 from graphene import relay
 from graphene.types.datetime import DateTime
@@ -134,9 +135,57 @@ class CreateReport(relay.ClientIDMutation):
         return CreateReport(report=types.Report.from_db(report))
 
 
+class UpdateReport(relay.ClientIDMutation):
+
+    class Input:
+        id = graphene.ID(required=True)
+        title = graphene.String(required=True)
+        body = graphene.String(required=True)
+        received_benefit = graphene.String()
+        provided_benefit = graphene.String()
+        our_participants = graphene.String()
+        other_participants = graphene.String()
+        date = DateTime(required=True)
+        is_draft = graphene.Boolean(default_value=False)
+
+    report = graphene.Field(types.Report)
+
+    @classmethod
+    def mutate_and_get_payload(cls, root, info, **input):
+        if not info.context.user.is_authenticated:
+            raise Exception('User must be logged in to perform this mutation.')
+
+        author = info.context.user
+        type, id = from_global_id(input.get('id'))
+
+        try:
+            report = Report.objects.select_related('author').get(id=id, author_id=author.id)
+        except Report.DoesNotExist:
+            raise Exception('Viewer is not the Author of this Report or Report does not exist.')
+
+        is_draft = input.get('is_draft')
+
+        if is_draft and not report.is_draft:
+            raise Exception('You cannot update published Report with draft.')
+
+        report.published = arrow.utcnow().datetime
+        report.date = input.get('date')
+        report.title = strip_all_tags(input.get('title', ''))
+        report.body = strip_all_tags(input.get('body', ''))
+        report.received_benefit = strip_all_tags(input.get('received_benefit', ''))
+        report.provided_benefit = strip_all_tags(input.get('provided_benefit', ''))
+        report.our_participants = strip_all_tags(input.get('our_participants', ''))
+        report.other_participants = strip_all_tags(input.get('other_participants', ''))
+        report.is_draft = is_draft
+        report.save()
+
+        return UpdateReport(report=types.Report.from_db(report))
+
+
 class Mutation:
     login = Login.Field()
     login_by_shortcut = LoginByShortcut.Field()
     # TODO
     # logout = Logout.Field()
     create_report = CreateReport.Field()
+    update_report = UpdateReport.Field()
diff --git a/tests/dummy.py b/tests/dummy.py
index ddd0fe018823076ad5c5201d44aa2b5e812d18c7..9e8a1be6305ee5ee938391a294618703efede2c5 100644
--- a/tests/dummy.py
+++ b/tests/dummy.py
@@ -103,3 +103,8 @@ def prepare_reports():
 
 def prepare_author():
     User.objects.create(**authors[0])
+
+
+def prepare_report(is_draft=False):
+    author = User.objects.create(**authors[0])
+    Report.objects.create(author=author, is_draft=is_draft, **reports[0])
diff --git a/tests/mutations/snapshots/snap_test_update_report.py b/tests/mutations/snapshots/snap_test_update_report.py
new file mode 100644
index 0000000000000000000000000000000000000000..18f47b8d0343843a2491a80a47b5539808704c58
--- /dev/null
+++ b/tests/mutations/snapshots/snap_test_update_report.py
@@ -0,0 +1,180 @@
+# -*- coding: utf-8 -*-
+# snapshottest: v1 - https://goo.gl/zC4yUc
+from __future__ import unicode_literals
+
+from snapshottest import Snapshot
+
+
+snapshots = Snapshot()
+
+snapshots['test_unauthorized 1'] = {
+    'data': {
+        'updateReport': None
+    },
+    'errors': [
+        {
+            'locations': [
+                {
+                    'column': 5,
+                    'line': 3
+                }
+            ],
+            'message': 'User must be logged in to perform this mutation.'
+        }
+    ]
+}
+
+snapshots['test_not_author 1'] = {
+    'data': {
+        'updateReport': None
+    },
+    'errors': [
+        {
+            'locations': [
+                {
+                    'column': 5,
+                    'line': 3
+                }
+            ],
+            'message': 'Viewer is not the Author of this Report or Report does not exist.'
+        }
+    ]
+}
+
+snapshots['test_report_does_not_exist 1'] = {
+    'data': {
+        'updateReport': None
+    },
+    'errors': [
+        {
+            'locations': [
+                {
+                    'column': 5,
+                    'line': 3
+                }
+            ],
+            'message': 'Viewer is not the Author of this Report or Report does not exist.'
+        }
+    ]
+}
+
+snapshots['test_update_published_with_draft 1'] = {
+    'data': {
+        'updateReport': None
+    },
+    'errors': [
+        {
+            'locations': [
+                {
+                    'column': 5,
+                    'line': 3
+                }
+            ],
+            'message': 'You cannot update published Report with draft.'
+        }
+    ]
+}
+
+snapshots['test_update_draft_with_draft 1'] = {
+    'data': {
+        'updateReport': {
+            'report': {
+                'author': {
+                    'extra': '{"movies": 1}',
+                    'firstName': 'Winston',
+                    'id': 'QXV0aG9yOjE=',
+                    'lastName': 'Wolfe',
+                    'totalReports': 0
+                },
+                'body': 'I visited Tesla factory and talked with Elon Musk.',
+                'date': '2018-03-03 00:00:00+00:00',
+                'extra': None,
+                'id': 'UmVwb3J0OjE=',
+                'otherParticipants': 'Elon Musk',
+                'ourParticipants': 'me',
+                'providedBenefit': 'nothing',
+                'published': '2018-03-08 00:00:00+00:00',
+                'receivedBenefit': 'Tesla Model S',
+                'title': 'Free Tesla'
+            }
+        }
+    }
+}
+
+snapshots['test_update_draft_with_published 1'] = {
+    'data': {
+        'updateReport': {
+            'report': {
+                'author': {
+                    'extra': '{"movies": 1}',
+                    'firstName': 'Winston',
+                    'id': 'QXV0aG9yOjE=',
+                    'lastName': 'Wolfe',
+                    'totalReports': 1
+                },
+                'body': 'I visited Tesla factory and talked with Elon Musk.',
+                'date': '2018-03-03 00:00:00+00:00',
+                'extra': None,
+                'id': 'UmVwb3J0OjE=',
+                'otherParticipants': 'Elon Musk',
+                'ourParticipants': 'me',
+                'providedBenefit': 'nothing',
+                'published': '2018-03-08 00:00:00+00:00',
+                'receivedBenefit': 'Tesla Model S',
+                'title': 'Free Tesla'
+            }
+        }
+    }
+}
+
+snapshots['test_update_published_with_published 1'] = {
+    'data': {
+        'updateReport': {
+            'report': {
+                'author': {
+                    'extra': '{"movies": 1}',
+                    'firstName': 'Winston',
+                    'id': 'QXV0aG9yOjE=',
+                    'lastName': 'Wolfe',
+                    'totalReports': 1
+                },
+                'body': 'I visited Tesla factory and talked with Elon Musk.',
+                'date': '2018-03-03 00:00:00+00:00',
+                'extra': None,
+                'id': 'UmVwb3J0OjE=',
+                'otherParticipants': 'Elon Musk',
+                'ourParticipants': 'me',
+                'providedBenefit': 'nothing',
+                'published': '2018-03-08 00:00:00+00:00',
+                'receivedBenefit': 'Tesla Model S',
+                'title': 'Free Tesla'
+            }
+        }
+    }
+}
+
+snapshots['test_input_sanitization 1'] = {
+    'data': {
+        'updateReport': {
+            'report': {
+                'author': {
+                    'extra': '{"movies": 1}',
+                    'firstName': 'Winston',
+                    'id': 'QXV0aG9yOjE=',
+                    'lastName': 'Wolfe',
+                    'totalReports': 1
+                },
+                'body': 'some link in body',
+                'date': '2018-03-03 00:00:00+00:00',
+                'extra': None,
+                'id': 'UmVwb3J0OjE=',
+                'otherParticipants': 'you!',
+                'ourParticipants': 'me, myself',
+                'providedBenefit': 'tea',
+                'published': '2018-03-08 00:00:00+00:00',
+                'receivedBenefit': 'coffee',
+                'title': 'No tags'
+            }
+        }
+    }
+}
diff --git a/tests/mutations/test_update_report.py b/tests/mutations/test_update_report.py
new file mode 100644
index 0000000000000000000000000000000000000000..c0e1ff2fbb87b8e8f19c26d0e6ed87cbbacc654b
--- /dev/null
+++ b/tests/mutations/test_update_report.py
@@ -0,0 +1,161 @@
+import pytest
+import arrow
+from graphql_relay import to_global_id
+from unittest.mock import patch
+
+from openlobby.core.models import User, Report
+
+from ..dummy import prepare_report
+from ..utils import call_api
+
+
+pytestmark = [pytest.mark.django_db, pytest.mark.usefixtures('django_es')]
+
+
+query = """
+mutation updateReport ($input: UpdateReportInput!) {
+    updateReport (input: $input) {
+        report {
+            id
+            date
+            published
+            title
+            body
+            receivedBenefit
+            providedBenefit
+            ourParticipants
+            otherParticipants
+            extra
+            author {
+                id
+                firstName
+                lastName
+                totalReports
+                extra
+            }
+        }
+    }
+}
+"""
+
+published = arrow.get(2018, 3, 8)
+
+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'
+
+
+def get_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(),
+        'isDraft': is_draft,
+    }
+
+
+def assert_report(is_draft=False):
+    report = Report.objects.get(id=1)
+    assert report.author_id == 1
+    assert report.date == date.datetime
+    assert report.published == published.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)
+    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')
+    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')
+    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')
+    snapshot.assert_match(response)
+
+
+def test_update_draft_with_draft(client, snapshot):
+    prepare_report(is_draft=True)
+    input = get_input(is_draft=True)
+    with patch('openlobby.core.api.mutations.arrow.utcnow', return_value=published):
+        response = call_api(client, query, input, 'wolf')
+    snapshot.assert_match(response)
+    assert_report(is_draft=True)
+
+
+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=published):
+        response = call_api(client, query, input, 'wolf')
+    snapshot.assert_match(response)
+    assert_report()
+
+
+def test_update_published_with_published(client, snapshot):
+    prepare_report()
+    input = get_input()
+    with patch('openlobby.core.api.mutations.arrow.utcnow', return_value=published):
+        response = call_api(client, query, input, 'wolf')
+    snapshot.assert_match(response)
+    assert_report()
+
+
+def test_input_sanitization(client, snapshot):
+    prepare_report()
+    input = {
+        'id': to_global_id('Report', 1),
+        '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(),
+    }
+
+    with patch('openlobby.core.api.mutations.arrow.utcnow', return_value=published):
+        response = call_api(client, query, input, 'wolf')
+
+    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!'