From d37847b67425421b781e1c870659eb907a96f447 Mon Sep 17 00:00:00 2001
From: mk2155 <mk2155@cam.ac.uk>
Date: Fri, 7 Feb 2025 13:49:00 +0000
Subject: [PATCH] fix: correct read only create behaviour

Read only create failed to work correctly due to the way DRF serializers
behave. Updates to work consistently, and adds test to cover this case.
---
 data_manager_api/tests/test_views.py | 37 +++++++++++++++++++++++++---
 data_manager_api/views.py            |  8 +++++-
 2 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/data_manager_api/tests/test_views.py b/data_manager_api/tests/test_views.py
index cc6a8e9..273d8a0 100644
--- a/data_manager_api/tests/test_views.py
+++ b/data_manager_api/tests/test_views.py
@@ -204,13 +204,10 @@ def test_account_delete_valid_at_earlier(api_client):
     assert Account.objects.filter(crsid=instance.crsid, deleted_at=None).exists()
 
 
-def test_read_only_mode(api_client, settings):
+def test_read_only_mode_delete(api_client, settings):
     settings.DATA_MANAGER_READ_ONLY = True
 
     account = AccountFactory(account_details=True, account_identifier=True)
-    account_details = account.account_details
-    account_identifier = account.account_identifier.first()
-
     valid_at = account.valid_at + timedelta(hours=1)
 
     response = api_client.delete(
@@ -223,6 +220,16 @@ def test_read_only_mode(api_client, settings):
     response = api_client.delete(build_account_detail_url("notpresent", valid_at))
 
     assert response.status_code == status.HTTP_204_NO_CONTENT
+    assert Account.objects.filter(crsid=account.crsid, deleted_at=None).exists()
+
+
+def test_read_only_mode_update(api_client, settings):
+    settings.DATA_MANAGER_READ_ONLY = True
+
+    account = AccountFactory(account_details=True, account_identifier=True)
+    account_details = account.account_details
+    account_identifier = account.account_identifier.first()
+    valid_at = account.valid_at + timedelta(hours=1)
 
     initial_data = data_account_factory(account, account_details, account_identifier)
     new_data = {
@@ -244,6 +251,28 @@ def test_read_only_mode(api_client, settings):
     assert Account.objects.get(crsid=account.crsid).account_details.name == initial_data["name"]
 
 
+def test_read_only_mode_create(api_client, settings):
+    settings.DATA_MANAGER_READ_ONLY = True
+
+    account = AccountFactory.build()
+
+    data = data_account_factory(account)
+
+    crsid = account.crsid
+    valid_at = account.valid_at
+
+    # Sanity-check: no record in DB
+    assert not Account.objects.filter(crsid=crsid).exists()
+    assert not AccountIdentifier.objects.filter(account_id=account).exists()
+
+    # We try to update while the object does not exist yet
+    response = api_client.put(build_account_detail_url(crsid, valid_at), data)
+
+    # We instead created the object
+    assert response.status_code == status.HTTP_201_CREATED
+    assert not Account.objects.filter(crsid=crsid).exists()
+
+
 @pytest.mark.parametrize("method", ("put", "delete"))
 def test_account_valid_at_missing(api_client, method):
     account_details = AccountDetailsFactory()
diff --git a/data_manager_api/views.py b/data_manager_api/views.py
index d305244..b4b176f 100644
--- a/data_manager_api/views.py
+++ b/data_manager_api/views.py
@@ -140,8 +140,14 @@ class AccountViewSet(viewsets.GenericViewSet):
                 # forcibly invalidate the prefetch cache on the instance.
                 instance._prefetched_objects_cache = {}
 
+        # If instance is not set on the serializer, attempting to call `serializer.data` will
+        # result in an exception, as the instance object passed to the to_representation call will
+        # be a dictionary.
+        # The instance will be set either if one exists in the database, or after a call to
+        # serializer.save.
+        response_data = serializer.data if serializer.instance else serializer.initial_data
         return Response(
-            serializer.data, status=status.HTTP_200_OK if instance else status.HTTP_201_CREATED
+            response_data, status=status.HTTP_200_OK if instance else status.HTTP_201_CREATED
         )
 
     def get_exception_handler(self):
-- 
GitLab