"""Tests for the release-level DMS carveout DELETE endpoints. Both routes are guarded by a grass-header check and then a grass-ownership check before anything is deleted (carveouts/handlers.py + carveouts/utils/utils.py): * Incomplete grass headers (type without id, or vice versa) are rejected with 400 *before* the ownership check -> always safe to assert. * A complete grass header for an account that does NOT own the product is rejected with 403 *before* the delete -> safe, but needs real data. * A complete grass header for the owning account actually deletes -> a true mutation, so it is gated behind the `write` marker and opt-in env vars. Note: a request with NO grass headers proceeds straight to the delete, so the suite never sends one. """ import pytest import requests from tests.integration.consts import api from tests.integration.utils import assert_ok # Both delete routes behave identically through the guard layers, so the # header/ownership tests run against each. _ROUTE_PARAMS = [ pytest.param(api.DELETE_RELEASE_DMS, id="release_dms"), pytest.param(api.DELETE_RELEASE_DMS_MASTER, id="release_dms_master"), ] # The grass validator rejects a partial header pair in either direction (it # checks that account type and id are both present or both absent), so both # incomplete combinations must be rejected identically. Note: an *invalid* # account type paired with an id is NOT caught here -- it passes the header # check and is rejected later by the ownership check (see the 403 test below). _INCOMPLETE_HEADER_PARAMS = [ pytest.param({api.GRASS_ACCOUNT_TYPE_HEADER: "vendor"}, id="type_without_id"), pytest.param({api.GRASS_ACCOUNT_ID_HEADER: "1"}, id="id_without_type"), ] requires_nonowner = pytest.mark.skipif( not (api.NONOWNER_PRODUCT_ID and api.NONOWNER_ACCOUNT_ID), reason="needs TEST_NONOWNER_PRODUCT_ID and TEST_NONOWNER_ACCOUNT_ID", ) requires_owner = pytest.mark.skipif( not (api.WRITE_PRODUCT_ID and api.WRITE_ACCOUNT_ID), reason="needs TEST_WRITE_PRODUCT_ID and TEST_WRITE_ACCOUNT_ID", ) @pytest.mark.negative @pytest.mark.parametrize("headers", _INCOMPLETE_HEADER_PARAMS) @pytest.mark.parametrize("url_template", _ROUTE_PARAMS) def test_delete_incomplete_grass_headers_rejected( url_template: str, headers: dict[str, str] ) -> None: """A partial grass header pair (either direction) is rejected with 400. This short-circuits before the ownership check and the delete, so it is safe against any environment and needs no real product. """ response = requests.delete( url_template.format(product_id=1), headers=headers, timeout=api.REQUEST_TIMEOUT, ) assert response.status_code == 400, response.url @pytest.mark.negative @requires_nonowner @pytest.mark.parametrize("url_template", _ROUTE_PARAMS) def test_delete_non_owner_forbidden(url_template: str) -> None: """A grass account that does not own the product is rejected with 403. The 403 short-circuits before the delete, so no data is mutated. """ headers = { api.GRASS_ACCOUNT_TYPE_HEADER: api.NONOWNER_ACCOUNT_TYPE, api.GRASS_ACCOUNT_ID_HEADER: api.NONOWNER_ACCOUNT_ID or "", } response = requests.delete( url_template.format(product_id=api.NONOWNER_PRODUCT_ID), headers=headers, timeout=api.REQUEST_TIMEOUT, ) assert response.status_code == 403, response.url @pytest.mark.write @requires_owner @pytest.mark.parametrize("url_template", _ROUTE_PARAMS) def test_delete_as_owner_succeeds(url_template: str) -> None: """The owning grass account can delete the product's DMS carveouts. This mutates data, so it only runs when throwaway owner/product env vars are supplied. """ headers = { api.GRASS_ACCOUNT_TYPE_HEADER: api.WRITE_ACCOUNT_TYPE, api.GRASS_ACCOUNT_ID_HEADER: api.WRITE_ACCOUNT_ID or "", } response = requests.delete( url_template.format(product_id=api.WRITE_PRODUCT_ID), headers=headers, timeout=api.REQUEST_TIMEOUT, ) assert_ok(response)