mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
fix(views): adhere to JSON:API for errors
This commit is contained in:
@@ -4375,15 +4375,21 @@ class TestResourceViewSet:
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == "invalid_provider"
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == "invalid_provider"
|
||||
assert error["status"] == "400" # Must be string per JSON:API spec
|
||||
assert error["source"]["pointer"] == "/data/attributes/provider"
|
||||
assert "AWS" in error["detail"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lookback_days,expected_status,expected_code",
|
||||
"lookback_days,expected_status,expected_code,expected_detail_contains",
|
||||
[
|
||||
("abc", status.HTTP_400_BAD_REQUEST, "invalid"), # Non-integer
|
||||
("0", status.HTTP_400_BAD_REQUEST, "out_of_range"), # Below min
|
||||
("91", status.HTTP_400_BAD_REQUEST, "out_of_range"), # Above max (90)
|
||||
("-5", status.HTTP_400_BAD_REQUEST, "out_of_range"), # Negative
|
||||
("abc", status.HTTP_400_BAD_REQUEST, "invalid", "valid integer"),
|
||||
("0", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 90"),
|
||||
("91", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 90"),
|
||||
("-5", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 90"),
|
||||
],
|
||||
)
|
||||
def test_events_invalid_lookback_days(
|
||||
@@ -4393,8 +4399,9 @@ class TestResourceViewSet:
|
||||
lookback_days,
|
||||
expected_status,
|
||||
expected_code,
|
||||
expected_detail_contains,
|
||||
):
|
||||
"""Test events endpoint validates lookback_days parameter."""
|
||||
"""Test events endpoint validates lookback_days with JSON:API compliant errors."""
|
||||
from api.models import Resource
|
||||
|
||||
aws_provider = providers_fixture[0] # AWS provider from fixture
|
||||
@@ -4415,7 +4422,60 @@ class TestResourceViewSet:
|
||||
)
|
||||
|
||||
assert response.status_code == expected_status
|
||||
assert response.json()["errors"][0]["code"] == expected_code
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == expected_code
|
||||
assert error["status"] == "400" # Must be string per JSON:API spec
|
||||
assert error["source"]["parameter"] == "lookback_days"
|
||||
assert expected_detail_contains in error["detail"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"page_size,expected_status,expected_code,expected_detail_contains",
|
||||
[
|
||||
("abc", status.HTTP_400_BAD_REQUEST, "invalid", "valid integer"),
|
||||
("0", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 50"),
|
||||
("51", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 50"),
|
||||
("-1", status.HTTP_400_BAD_REQUEST, "out_of_range", "between 1 and 50"),
|
||||
],
|
||||
)
|
||||
def test_events_invalid_page_size(
|
||||
self,
|
||||
authenticated_client,
|
||||
providers_fixture,
|
||||
page_size,
|
||||
expected_status,
|
||||
expected_code,
|
||||
expected_detail_contains,
|
||||
):
|
||||
"""Test events endpoint validates page[size] with JSON:API compliant errors."""
|
||||
from api.models import Resource
|
||||
|
||||
aws_provider = providers_fixture[0] # AWS provider from fixture
|
||||
|
||||
resource = Resource.objects.create(
|
||||
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-pagesize-test",
|
||||
name="Test Instance",
|
||||
type="instance",
|
||||
region="us-east-1",
|
||||
service="ec2",
|
||||
provider=aws_provider,
|
||||
tenant_id=aws_provider.tenant_id,
|
||||
)
|
||||
|
||||
response = authenticated_client.get(
|
||||
reverse("resource-events", kwargs={"pk": resource.id}),
|
||||
{"page[size]": page_size},
|
||||
)
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == expected_code
|
||||
assert error["status"] == "400" # Must be string per JSON:API spec
|
||||
assert error["source"]["parameter"] == "page[size]"
|
||||
assert expected_detail_contains in error["detail"]
|
||||
|
||||
@patch("api.v1.views.initialize_prowler_provider")
|
||||
@patch("api.v1.views.CloudTrailTimeline")
|
||||
@@ -4485,6 +4545,14 @@ class TestResourceViewSet:
|
||||
events = response_data["data"]
|
||||
|
||||
assert len(events) == 2
|
||||
|
||||
# Verify JSON:API structure: type and id are present
|
||||
assert events[0]["type"] == "resource-events"
|
||||
assert events[0]["id"] == "event-1-id"
|
||||
assert events[1]["type"] == "resource-events"
|
||||
assert events[1]["id"] == "event-2-id"
|
||||
|
||||
# Verify attributes
|
||||
assert events[0]["attributes"]["event_name"] == "RunInstances"
|
||||
assert events[0]["attributes"]["actor"] == "admin@example.com"
|
||||
assert events[1]["attributes"]["event_name"] == "StopInstances"
|
||||
@@ -4582,7 +4650,12 @@ class TestResourceViewSet:
|
||||
|
||||
# 502 because this is an upstream auth failure, not API auth failure
|
||||
assert response.status_code == status.HTTP_502_BAD_GATEWAY
|
||||
assert response.json()["errors"][0]["code"] == "upstream_auth_failed"
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == "upstream_auth_failed"
|
||||
assert error["status"] == "502" # Must be string per JSON:API spec
|
||||
assert "detail" in error
|
||||
|
||||
@patch("api.v1.views.initialize_prowler_provider")
|
||||
@patch("api.v1.views.CloudTrailTimeline")
|
||||
@@ -4628,7 +4701,12 @@ class TestResourceViewSet:
|
||||
|
||||
# AccessDenied returns 502 (upstream error, not user's fault)
|
||||
assert response.status_code == status.HTTP_502_BAD_GATEWAY
|
||||
assert response.json()["errors"][0]["code"] == "upstream_access_denied"
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == "upstream_access_denied"
|
||||
assert error["status"] == "502" # Must be string per JSON:API spec
|
||||
assert "detail" in error
|
||||
|
||||
@patch("api.v1.views.initialize_prowler_provider")
|
||||
@patch("api.v1.views.CloudTrailTimeline")
|
||||
@@ -4674,7 +4752,12 @@ class TestResourceViewSet:
|
||||
|
||||
# Non-AccessDenied errors return 503
|
||||
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
assert response.json()["errors"][0]["code"] == "service_unavailable"
|
||||
|
||||
# Verify JSON:API error structure
|
||||
error = response.json()["errors"][0]
|
||||
assert error["code"] == "service_unavailable"
|
||||
assert error["status"] == "503" # Must be string per JSON:API spec
|
||||
assert "detail" in error
|
||||
|
||||
def test_events_unauthenticated_returns_401(self, providers_fixture):
|
||||
"""Test events endpoint returns 401 when no credentials are provided.
|
||||
|
||||
@@ -2922,8 +2922,14 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
|
||||
lookback_days = int(lookback_days_str)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError(
|
||||
{"lookback_days": "Must be a valid integer"},
|
||||
code="invalid",
|
||||
[
|
||||
{
|
||||
"detail": "lookback_days must be a valid integer",
|
||||
"status": "400",
|
||||
"source": {"parameter": "lookback_days"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
if not (
|
||||
@@ -2932,13 +2938,17 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
|
||||
<= self.EVENTS_MAX_LOOKBACK_DAYS
|
||||
):
|
||||
raise ValidationError(
|
||||
{
|
||||
"lookback_days": (
|
||||
f"Must be between {self.EVENTS_MIN_LOOKBACK_DAYS} "
|
||||
f"and {self.EVENTS_MAX_LOOKBACK_DAYS}"
|
||||
)
|
||||
},
|
||||
code="out_of_range",
|
||||
[
|
||||
{
|
||||
"detail": (
|
||||
f"lookback_days must be between {self.EVENTS_MIN_LOOKBACK_DAYS} "
|
||||
f"and {self.EVENTS_MAX_LOOKBACK_DAYS}"
|
||||
),
|
||||
"status": "400",
|
||||
"source": {"parameter": "lookback_days"},
|
||||
"code": "out_of_range",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Validate and parse page[size] from query params (JSON:API pagination)
|
||||
@@ -2950,21 +2960,31 @@ class ResourceViewSet(PaginateByPkMixin, BaseRLSViewSet):
|
||||
page_size = int(page_size_str)
|
||||
except (ValueError, TypeError):
|
||||
raise ValidationError(
|
||||
{"page[size]": "Must be a valid integer"},
|
||||
code="invalid",
|
||||
[
|
||||
{
|
||||
"detail": "page[size] must be a valid integer",
|
||||
"status": "400",
|
||||
"source": {"parameter": "page[size]"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
if not (
|
||||
self.EVENTS_MIN_PAGE_SIZE <= page_size <= self.EVENTS_MAX_PAGE_SIZE
|
||||
):
|
||||
raise ValidationError(
|
||||
{
|
||||
"page[size]": (
|
||||
f"Must be between {self.EVENTS_MIN_PAGE_SIZE} "
|
||||
f"and {self.EVENTS_MAX_PAGE_SIZE}"
|
||||
)
|
||||
},
|
||||
code="out_of_range",
|
||||
[
|
||||
{
|
||||
"detail": (
|
||||
f"page[size] must be between {self.EVENTS_MIN_PAGE_SIZE} "
|
||||
f"and {self.EVENTS_MAX_PAGE_SIZE}"
|
||||
),
|
||||
"status": "400",
|
||||
"source": {"parameter": "page[size]"},
|
||||
"code": "out_of_range",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Parse include_read_events (default: false)
|
||||
|
||||
Reference in New Issue
Block a user