From 2f7c9d454f44783b5710bb3866c6a819ac67e119 Mon Sep 17 00:00:00 2001 From: Dawn Pattison Date: Thu, 23 Jun 2022 13:19:23 -0500 Subject: [PATCH] Revoke a Pending Privacy Request [#525] (#592) * Add the ability to cancel a pending task. The celery task is not actually cancelled yet. - Track cancel reason, datetime cancelled, and add a new cancelled status. * Add drp revoke request to postman collection. * Add drp revoke docs. * Update down_rev after rebase. * Fix incorrect check. * Restore new canceled state. * Check that the privacy request is not canceled right before starting execution. This is really our last chance to check before we start executing the graph in dask. The use case here might be it was canceled shortly after it was approved. * Attempt to revoke a queued celery task if we cancel it before it starts executing. * Prettier. * Changelog updated. * Add a few unit tests around how triggering the run_privacy_request_task with a cancelled task id doesn't do anything and how you can't approve a canceled privacy request. * Fix SQLAlchemy logging to console - logging in migration propagates to the rest of the application. * Refresh session instead of creating a new one. * Add 200 character limit. * Add some assertions that db.refresh is doing what we think it's doing. --- CHANGELOG.md | 1 + .../features/common/RequestStatusBadge.tsx | 4 ++ .../privacy-requests/RequestFilters.tsx | 1 + .../src/features/privacy-requests/types.ts | 1 + .../docs/guides/data_rights_protocol.md | 11 ++++ .../postman/Fidesops.postman_collection.json | 37 +++++++++++ .../api/v1/endpoints/drp_endpoints.py | 40 +++++++++++- src/fidesops/api/v1/urn_registry.py | 1 + .../55d61eb8ed12_add_default_policies.py | 1 - .../ed1b00ff963d_cancel_privacy_request.py | 45 +++++++++++++ src/fidesops/models/privacy_request.py | 19 ++++++ src/fidesops/schemas/drp_privacy_request.py | 7 +++ .../service/drp/drp_fidesops_mapper.py | 1 + .../privacy_request/request_runner_service.py | 5 ++ tests/api/v1/endpoints/test_drp_endpoints.py | 63 +++++++++++++++++++ .../test_privacy_request_endpoints.py | 21 +++++-- tests/fixtures/application_fixtures.py | 13 ++++ tests/integration_tests/test_execution.py | 1 - .../request_runner_service_test.py | 55 +++++++++------- 19 files changed, 299 insertions(+), 28 deletions(-) create mode 100644 src/fidesops/migrations/versions/ed1b00ff963d_cancel_privacy_request.py diff --git a/CHANGELOG.md b/CHANGELOG.md index f4353fb889..ff4bf9a630 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The types of changes are: * Added the ability to delete a datastore from the frontend [#683] https://github.com/ethyca/fidesops/pull/683 * Added the ability to disable/enable a datastore from the frontend [#693] https://github.com/ethyca/fidesops/pull/693 * Adds Postgres and Redis health checks to health endpoint [#690](https://github.com/ethyca/fidesops/pull/690) +* Adds the ability to revoke a pending privacy request [#592](https://github.com/ethyca/fidesops/pull/592/files) * Added health checks and better error messages on app startup for both db and cache [#686](https://github.com/ethyca/fidesops/pull/686) * Datastore Connection Filters [#691](https://github.com/ethyca/fidesops/pull/691) diff --git a/clients/admin-ui/src/features/common/RequestStatusBadge.tsx b/clients/admin-ui/src/features/common/RequestStatusBadge.tsx index c688fd5403..fef7e7715f 100644 --- a/clients/admin-ui/src/features/common/RequestStatusBadge.tsx +++ b/clients/admin-ui/src/features/common/RequestStatusBadge.tsx @@ -18,6 +18,10 @@ export const statusPropMap: { bg: "red.500", label: "Denied", }, + canceled: { + bg: "red.600", + label: "Canceled", + }, error: { bg: "red.800", label: "Error", diff --git a/clients/admin-ui/src/features/privacy-requests/RequestFilters.tsx b/clients/admin-ui/src/features/privacy-requests/RequestFilters.tsx index 7e40f50f61..c70903db7d 100644 --- a/clients/admin-ui/src/features/privacy-requests/RequestFilters.tsx +++ b/clients/admin-ui/src/features/privacy-requests/RequestFilters.tsx @@ -107,6 +107,7 @@ const RequestFilters: React.FC = () => { + diff --git a/clients/admin-ui/src/features/privacy-requests/types.ts b/clients/admin-ui/src/features/privacy-requests/types.ts index 6dc70ab4d3..476ad2dce6 100644 --- a/clients/admin-ui/src/features/privacy-requests/types.ts +++ b/clients/admin-ui/src/features/privacy-requests/types.ts @@ -5,6 +5,7 @@ export type PrivacyRequestStatus = | "error" | "in_processing" | "paused" + | "canceled" | "pending"; export enum ActionType { diff --git a/docs/fidesops/docs/guides/data_rights_protocol.md b/docs/fidesops/docs/guides/data_rights_protocol.md index 376a21d782..6af7282cb9 100644 --- a/docs/fidesops/docs/guides/data_rights_protocol.md +++ b/docs/fidesops/docs/guides/data_rights_protocol.md @@ -85,4 +85,15 @@ All data rights associated with existing policies may be returned via the `/data ], "user_relationships": null } +``` + +### Revoke + +You can revoke a pending privacy request via the `/revoke` endpoint. + +```json title="GET /api/v1/drp/revoke" +{ + "request_id": "c789ff35-7644-4ceb-9981-4b35c264aac3", + "reason": "Accidentally submitted" +} ``` \ No newline at end of file diff --git a/docs/fidesops/docs/postman/Fidesops.postman_collection.json b/docs/fidesops/docs/postman/Fidesops.postman_collection.json index a184267d69..16f4217bb9 100644 --- a/docs/fidesops/docs/postman/Fidesops.postman_collection.json +++ b/docs/fidesops/docs/postman/Fidesops.postman_collection.json @@ -3508,6 +3508,43 @@ } }, "response": [] + }, + { + "name": "Revoke request", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{client_token}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"request_id\": \"{{privacy_request_id}}\", \n \"reason\": \"Accidentally submitted\"\n\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{host}}/drp/revoke", + "host": [ + "{{host}}" + ], + "path": [ + "drp", + "revoke" + ] + } + }, + "response": [] } ] }, diff --git a/src/fidesops/api/v1/endpoints/drp_endpoints.py b/src/fidesops/api/v1/endpoints/drp_endpoints.py index 736c750173..cd678d8c75 100644 --- a/src/fidesops/api/v1/endpoints/drp_endpoints.py +++ b/src/fidesops/api/v1/endpoints/drp_endpoints.py @@ -6,6 +6,7 @@ from sqlalchemy.orm import Session from starlette.status import ( HTTP_200_OK, + HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY, HTTP_424_FAILED_DEPENDENCY, @@ -16,14 +17,18 @@ from fidesops.api import deps from fidesops.api.v1 import scope_registry as scopes from fidesops.api.v1 import urn_registry as urls +from fidesops.api.v1.endpoints.privacy_request_endpoints import ( + get_privacy_request_or_error, +) from fidesops.core.config import config from fidesops.models.policy import DrpAction, Policy -from fidesops.models.privacy_request import PrivacyRequest +from fidesops.models.privacy_request import PrivacyRequest, PrivacyRequestStatus from fidesops.schemas.drp_privacy_request import ( DRP_VERSION, DrpDataRightsResponse, DrpIdentity, DrpPrivacyRequestCreate, + DrpRevokeRequest, ) from fidesops.schemas.privacy_request import PrivacyRequestDRPStatusResponse from fidesops.schemas.redis_cache import PrivacyRequestIdentity @@ -176,3 +181,36 @@ def get_drp_data_rights(*, db: Session = Depends(deps.get_db)) -> DrpDataRightsR return DrpDataRightsResponse( version=DRP_VERSION, api_base=None, actions=actions, user_relationships=None ) + + +@router.post( + urls.DRP_REVOKE, + dependencies=[ + Security(verify_oauth_client, scopes=[scopes.PRIVACY_REQUEST_REVIEW]) + ], + response_model=PrivacyRequestDRPStatusResponse, +) +def revoke_request( + *, db: Session = Depends(deps.get_db), data: DrpRevokeRequest +) -> PrivacyRequestDRPStatusResponse: + """ + Revoke a pending privacy request. + """ + privacy_request: PrivacyRequest = get_privacy_request_or_error(db, data.request_id) + + if privacy_request.status != PrivacyRequestStatus.pending: + raise HTTPException( + status_code=HTTP_400_BAD_REQUEST, + detail=f"Invalid revoke request. Can only revoke `pending` requests. " + f"Privacy request '{privacy_request.id}' status = {privacy_request.status.value}.", + ) + + logger.info(f"Canceling privacy request '{privacy_request.id}'") + privacy_request.cancel_processing(db, cancel_reason=data.reason) + + return PrivacyRequestDRPStatusResponse( + request_id=privacy_request.id, + received_at=privacy_request.requested_at, + status=DrpFidesopsMapper.map_status(privacy_request.status), + reason=data.reason, + ) diff --git a/src/fidesops/api/v1/urn_registry.py b/src/fidesops/api/v1/urn_registry.py index 31d8a87457..e59374a457 100644 --- a/src/fidesops/api/v1/urn_registry.py +++ b/src/fidesops/api/v1/urn_registry.py @@ -101,3 +101,4 @@ DRP_EXERCISE = "/drp/exercise" DRP_STATUS = "/drp/status" DRP_DATA_RIGHTS = "/drp/data-rights" +DRP_REVOKE = "/drp/revoke" diff --git a/src/fidesops/migrations/versions/55d61eb8ed12_add_default_policies.py b/src/fidesops/migrations/versions/55d61eb8ed12_add_default_policies.py index 74e1eb1d5f..1a287456d0 100644 --- a/src/fidesops/migrations/versions/55d61eb8ed12_add_default_policies.py +++ b/src/fidesops/migrations/versions/55d61eb8ed12_add_default_policies.py @@ -37,7 +37,6 @@ logging.basicConfig() logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) -logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) revision = "55d61eb8ed12" down_revision = "b3b68c87c4a0" diff --git a/src/fidesops/migrations/versions/ed1b00ff963d_cancel_privacy_request.py b/src/fidesops/migrations/versions/ed1b00ff963d_cancel_privacy_request.py new file mode 100644 index 0000000000..07c0598b9d --- /dev/null +++ b/src/fidesops/migrations/versions/ed1b00ff963d_cancel_privacy_request.py @@ -0,0 +1,45 @@ +"""cancel privacy request + +Revision ID: ed1b00ff963d +Revises: 55d61eb8ed12 +Create Date: 2022-06-03 15:45:14.584540 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "ed1b00ff963d" +down_revision = "55d61eb8ed12" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column( + "privacyrequest", sa.Column("cancel_reason", sa.String(200), nullable=True) + ) + op.add_column( + "privacyrequest", + sa.Column("canceled_at", sa.DateTime(timezone=True), nullable=True), + ) + op.execute("alter type privacyrequeststatus add value 'canceled'") + + +def downgrade(): + op.drop_column("privacyrequest", "canceled_at") + op.drop_column("privacyrequest", "cancel_reason") + + op.execute("delete from privacyrequest where status in ('canceled')") + + op.execute("alter type privacyrequeststatus rename to privacyrequeststatus_old") + op.execute( + "create type privacyrequeststatus as enum('in_processing', 'complete', 'pending', 'error', 'paused', 'approved', 'denied')" + ) + op.execute( + ( + "alter table privacyrequest alter column status type privacyrequeststatus using " + "status::text::privacyrequeststatus" + ) + ) + op.execute("drop type privacyrequeststatus_old") diff --git a/src/fidesops/models/privacy_request.py b/src/fidesops/models/privacy_request.py index 2fbf6369b6..4c4f002f90 100644 --- a/src/fidesops/models/privacy_request.py +++ b/src/fidesops/models/privacy_request.py @@ -38,6 +38,7 @@ ) from fidesops.schemas.masking.masking_secrets import MaskingSecretCache from fidesops.schemas.redis_cache import PrivacyRequestIdentity +from fidesops.tasks import celery_app from fidesops.util.cache import ( FidesopsRedis, get_all_cache_keys_for_privacy_request, @@ -100,6 +101,7 @@ class PrivacyRequestStatus(str, EnumType): in_processing = "in_processing" complete = "complete" paused = "paused" + canceled = "canceled" error = "error" @@ -158,6 +160,9 @@ class PrivacyRequest(Base): # pylint: disable=R0904 backref="privacy_requests", ) + cancel_reason = Column(String(200)) + canceled_at = Column(DateTime(timezone=True), nullable=True) + # passive_deletes="all" prevents execution logs from having their privacy_request_id set to null when # a privacy_request is deleted. We want to retain for record-keeping. execution_logs = relationship( @@ -461,6 +466,20 @@ def pause_processing(self, db: Session) -> None: }, ) + def cancel_processing(self, db: Session, cancel_reason: Optional[str]) -> None: + """Cancels a privacy request. Currently should only cancel 'pending' tasks""" + if self.canceled_at is None: + self.status = PrivacyRequestStatus.canceled + self.cancel_reason = cancel_reason + self.canceled_at = datetime.utcnow() + self.save(db) + + task_id = self.get_cached_task_id() + if task_id: + logger.info(f"Revoking task {task_id} for request {self.id}") + # Only revokes if execution is not already in progress + celery_app.control.revoke(task_id, terminate=False) + def error_processing(self, db: Session) -> None: """Mark privacy request as errored, and note time processing was finished""" self.update( diff --git a/src/fidesops/schemas/drp_privacy_request.py b/src/fidesops/schemas/drp_privacy_request.py index 02aa54d76e..e824a6e27a 100644 --- a/src/fidesops/schemas/drp_privacy_request.py +++ b/src/fidesops/schemas/drp_privacy_request.py @@ -66,3 +66,10 @@ class DrpDataRightsResponse(BaseSchema): api_base: Optional[str] actions: List[DrpAction] user_relationships: Optional[List[str]] + + +class DrpRevokeRequest(BaseSchema): + """DRP Data Rights Revoke Request Body""" + + request_id: str + reason: Optional[str] diff --git a/src/fidesops/service/drp/drp_fidesops_mapper.py b/src/fidesops/service/drp/drp_fidesops_mapper.py index 16b45c599b..0ba32ab683 100644 --- a/src/fidesops/service/drp/drp_fidesops_mapper.py +++ b/src/fidesops/service/drp/drp_fidesops_mapper.py @@ -50,6 +50,7 @@ def map_status( PrivacyRequestStatus.complete: PrivacyRequestDRPStatus.fulfilled, PrivacyRequestStatus.paused: PrivacyRequestDRPStatus.in_progress, PrivacyRequestStatus.error: PrivacyRequestDRPStatus.expired, + PrivacyRequestStatus.canceled: PrivacyRequestDRPStatus.revoked, } try: return PRIVACY_REQUEST_STATUS_TO_DRP_MAPPING[status] diff --git a/src/fidesops/service/privacy_request/request_runner_service.py b/src/fidesops/service/privacy_request/request_runner_service.py index e373987a3e..487b8122e3 100644 --- a/src/fidesops/service/privacy_request/request_runner_service.py +++ b/src/fidesops/service/privacy_request/request_runner_service.py @@ -173,6 +173,11 @@ def run_privacy_request( with SessionLocal() as session: privacy_request = PrivacyRequest.get(db=session, id=privacy_request_id) + if privacy_request.status == PrivacyRequestStatus.canceled: + logging.info( + f"Terminating privacy request {privacy_request.id}: request canceled." + ) + return logging.info(f"Dispatching privacy request {privacy_request.id}") privacy_request.start_processing(session) diff --git a/tests/api/v1/endpoints/test_drp_endpoints.py b/tests/api/v1/endpoints/test_drp_endpoints.py index eb6aa648b7..82f9d6f6c0 100644 --- a/tests/api/v1/endpoints/test_drp_endpoints.py +++ b/tests/api/v1/endpoints/test_drp_endpoints.py @@ -9,11 +9,13 @@ from fidesops.api.v1.scope_registry import ( POLICY_READ, PRIVACY_REQUEST_READ, + PRIVACY_REQUEST_REVIEW, STORAGE_CREATE_OR_UPDATE, ) from fidesops.api.v1.urn_registry import ( DRP_DATA_RIGHTS, DRP_EXERCISE, + DRP_REVOKE, DRP_STATUS, V1_URL_PREFIX, ) @@ -424,3 +426,64 @@ def test_get_drp_data_rights_multiple_drp_policies( ) assert 200 == response.status_code assert response.json() == expected_response + + +class TestDrpRevoke: + @pytest.fixture(scope="function") + def url(self) -> str: + return V1_URL_PREFIX + DRP_REVOKE + + def test_revoke_not_authenticated( + self, api_client: TestClient, privacy_request, url + ): + response = api_client.post(url, headers={}) + assert 401 == response.status_code + + def test_revoke_wrong_scope( + self, api_client: TestClient, generate_auth_header, url + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_READ]) + response = api_client.post(url, headers=auth_header, json={}) + assert 403 == response.status_code + + def test_revoke_wrong_status( + self, db, api_client: TestClient, generate_auth_header, url, privacy_request + ): + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_REVIEW]) + response = api_client.post( + url, headers=auth_header, json={"request_id": privacy_request.id} + ) + assert 400 == response.status_code + assert response.json()[ + "detail" + ] == "Invalid revoke request. Can only revoke `pending` requests. Privacy request '{}' status = in_processing.".format( + privacy_request.id + ) + db.refresh(privacy_request) + assert privacy_request.status == PrivacyRequestStatus.in_processing + assert privacy_request.canceled_at is None + + def test_revoke( + self, db, api_client: TestClient, generate_auth_header, url, privacy_request + ): + privacy_request.status = PrivacyRequestStatus.pending + privacy_request.save(db) + canceled_reason = "Accidentally submitted" + + auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_REVIEW]) + response = api_client.post( + url, + headers=auth_header, + json={"request_id": privacy_request.id, "reason": canceled_reason}, + ) + assert 200 == response.status_code + db.refresh(privacy_request) + + assert privacy_request.status == PrivacyRequestStatus.canceled + assert privacy_request.cancel_reason == canceled_reason + assert privacy_request.canceled_at is not None + + data = response.json() + assert data["request_id"] == privacy_request.id + assert data["status"] == "revoked" + assert data["reason"] == canceled_reason diff --git a/tests/api/v1/endpoints/test_privacy_request_endpoints.py b/tests/api/v1/endpoints/test_privacy_request_endpoints.py index 5128b90984..567716eadd 100644 --- a/tests/api/v1/endpoints/test_privacy_request_endpoints.py +++ b/tests/api/v1/endpoints/test_privacy_request_endpoints.py @@ -1432,13 +1432,24 @@ def test_approve_privacy_request_does_not_exist( ) assert not submit_mock.called + @pytest.mark.parametrize( + "privacy_request_status", + [PrivacyRequestStatus.complete, PrivacyRequestStatus.canceled], + ) @mock.patch( "fidesops.service.privacy_request.request_runner_service.run_privacy_request.delay" ) - def test_approve_completed_privacy_request( - self, submit_mock, db, url, api_client, generate_auth_header, privacy_request + def test_approve_privacy_request_in_non_pending_state( + self, + submit_mock, + db, + url, + api_client, + generate_auth_header, + privacy_request, + privacy_request_status, ): - privacy_request.status = PrivacyRequestStatus.complete + privacy_request.status = privacy_request_status privacy_request.save(db=db) auth_header = generate_auth_header(scopes=[PRIVACY_REQUEST_REVIEW]) @@ -1450,7 +1461,9 @@ def test_approve_completed_privacy_request( assert response_body["succeeded"] == [] assert len(response_body["failed"]) == 1 assert response_body["failed"][0]["message"] == "Cannot transition status" - assert response_body["failed"][0]["data"]["status"] == "complete" + assert ( + response_body["failed"][0]["data"]["status"] == privacy_request_status.value + ) assert not submit_mock.called @mock.patch( diff --git a/tests/fixtures/application_fixtures.py b/tests/fixtures/application_fixtures.py index db16e3f62c..ba60c2f180 100644 --- a/tests/fixtures/application_fixtures.py +++ b/tests/fixtures/application_fixtures.py @@ -799,6 +799,19 @@ def privacy_request_status_pending(db: Session, policy: Policy) -> PrivacyReques privacy_request.delete(db) +@pytest.fixture(scope="function") +def privacy_request_status_canceled(db: Session, policy: Policy) -> PrivacyRequest: + privacy_request = _create_privacy_request_for_policy( + db, + policy, + PrivacyRequestStatus.canceled, + ) + privacy_request.started_processing_at = None + privacy_request.save(db) + yield privacy_request + privacy_request.delete(db) + + @pytest.fixture(scope="function") def privacy_request_with_drp_action( db: Session, policy_drp_action: Policy diff --git a/tests/integration_tests/test_execution.py b/tests/integration_tests/test_execution.py index c9864ca7a9..a0b2f6fd5f 100644 --- a/tests/integration_tests/test_execution.py +++ b/tests/integration_tests/test_execution.py @@ -5,7 +5,6 @@ from pydantic import ValidationError from sqlalchemy.exc import InvalidRequestError -from fidesops.core.config import config from fidesops.db.session import get_db_session from fidesops.graph.config import CollectionAddress from fidesops.graph.graph import DatasetGraph diff --git a/tests/service/privacy_request/request_runner_service_test.py b/tests/service/privacy_request/request_runner_service_test.py index 48beeecf7d..fdac688fab 100644 --- a/tests/service/privacy_request/request_runner_service_test.py +++ b/tests/service/privacy_request/request_runner_service_test.py @@ -12,7 +12,6 @@ from fidesops.common_exceptions import ClientUnsuccessfulException, PrivacyRequestPaused from fidesops.core.config import config -from fidesops.db.session import get_db_session from fidesops.models.policy import PausedStep, PolicyPostWebhook from fidesops.models.privacy_request import ( ActionType, @@ -66,18 +65,15 @@ def test_start_processing_sets_started_processing_at( privacy_request_status_pending: PrivacyRequest, run_privacy_request_task, ) -> None: + updated_at = privacy_request_status_pending.updated_at assert privacy_request_status_pending.started_processing_at is None run_privacy_request_task.delay(privacy_request_status_pending.id).get( timeout=PRIVACY_REQUEST_TASK_TIMEOUT ) - _sessionmaker = get_db_session() - db = _sessionmaker() - assert ( - PrivacyRequest.get( - db=db, id=privacy_request_status_pending.id - ).started_processing_at - is not None - ) + + db.refresh(privacy_request_status_pending) + assert privacy_request_status_pending.started_processing_at is not None + assert privacy_request_status_pending.updated_at > updated_at def test_start_processing_doesnt_overwrite_started_processing_at( @@ -87,16 +83,35 @@ def test_start_processing_doesnt_overwrite_started_processing_at( ) -> None: before = privacy_request.started_processing_at assert before is not None + updated_at = privacy_request.updated_at run_privacy_request_task.delay(privacy_request.id).get( timeout=PRIVACY_REQUEST_TASK_TIMEOUT ) - _sessionmaker = get_db_session() - db = _sessionmaker() - - privacy_request = PrivacyRequest.get(db=db, id=privacy_request.id) + db.refresh(privacy_request) assert privacy_request.started_processing_at == before + assert privacy_request.updated_at > updated_at + + +@mock.patch( + "fidesops.service.privacy_request.request_runner_service.upload_access_results" +) +def test_halts_proceeding_if_cancelled( + upload_access_results_mock, + db: Session, + privacy_request_status_canceled: PrivacyRequest, + run_privacy_request_task, +) -> None: + assert privacy_request_status_canceled.status == PrivacyRequestStatus.canceled + run_privacy_request_task.delay(privacy_request_status_canceled.id).get( + timeout=PRIVACY_REQUEST_TASK_TIMEOUT + ) + db.refresh(privacy_request_status_canceled) + reloaded_pr = PrivacyRequest.get(db=db, id=privacy_request_status_canceled.id) + assert reloaded_pr.started_processing_at is None + assert reloaded_pr.status == PrivacyRequestStatus.canceled + assert not upload_access_results_mock.called @mock.patch( @@ -118,17 +133,16 @@ def test_from_graph_resume_does_not_run_pre_webhooks( privacy_request.started_processing_at = None privacy_request.policy = erasure_policy privacy_request.save(db) + updated_at = privacy_request.updated_at run_privacy_request_task.delay( privacy_request_id=privacy_request.id, from_step=PausedStep.access.value, ).get(timeout=PRIVACY_REQUEST_TASK_TIMEOUT) - _sessionmaker = get_db_session() - db = _sessionmaker() - - privacy_request = PrivacyRequest.get(db=db, id=privacy_request.id) + db.refresh(privacy_request) assert privacy_request.started_processing_at is not None + assert privacy_request.updated_at > updated_at # Starting privacy request in the middle of the graph means we don't run pre-webhooks again assert run_webhooks.call_count == 1 @@ -157,17 +171,16 @@ def test_resume_privacy_request_from_erasure( privacy_request.started_processing_at = None privacy_request.policy = erasure_policy privacy_request.save(db) + updated_at = privacy_request.updated_at run_privacy_request_task.delay( privacy_request_id=privacy_request.id, from_step=PausedStep.erasure.value, ).get(timeout=PRIVACY_REQUEST_TASK_TIMEOUT) - _sessionmaker = get_db_session() - db = _sessionmaker() - - privacy_request = PrivacyRequest.get(db=db, id=privacy_request.id) + db.refresh(privacy_request) assert privacy_request.started_processing_at is not None + assert privacy_request.updated_at > updated_at # Starting privacy request in the middle of the graph means we don't run pre-webhooks again assert run_webhooks.call_count == 1