diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index aeb87d3a59..351d23673d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -27,7 +27,7 @@ Please add a detailed description of how to review this PR. - [ ] Review if the code is being covered by tests. - [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings - [ ] Review if backport is needed. -- [ ] Review if is needed to change the [Readme.md](https://github.com/prowler-cloud/prowler/blob/master/README.md) +- [ ] Review if is needed to change the [README.md](https://github.com/prowler-cloud/prowler/blob/master/README.md) - [ ] Ensure a changelog fragment is added under [prowler/changelog.d/](https://github.com/prowler-cloud/prowler/tree/master/prowler/changelog.d), if applicable. #### SDK/CLI diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index 4d1c40fe23..c68d95d2b6 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -187,7 +187,7 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi @pytest.mark.django_db class TestTokenSwitchTenant: - def test_switch_tenant_with_valid_token(self, tenants_fixture, providers_fixture): + def test_switch_tenant_with_valid_token(self, tenants_fixture, aws_provider): client = APIClient() test_user = "test_email@prowler.com" @@ -1396,7 +1396,7 @@ class TestAPIKeyMultiTenantWorkflows: assert me_response2.json()["data"]["id"] == str(user.id) def test_api_key_cannot_access_different_tenant_resources( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """API key from one tenant cannot access resources from another tenant. diff --git a/api/src/backend/api/tests/test_decorators.py b/api/src/backend/api/tests/test_decorators.py index 25053a2258..5c2897730f 100644 --- a/api/src/backend/api/tests/test_decorators.py +++ b/api/src/backend/api/tests/test_decorators.py @@ -40,10 +40,10 @@ class TestSetTenantDecorator: @pytest.mark.django_db class TestHandleProviderDeletionDecorator: - def test_success_no_exception(self, tenants_fixture, providers_fixture): + def test_success_no_exception(self, tenants_fixture, aws_provider): """Decorated function runs normally when no exception is raised.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider @handle_provider_deletion def task_func(**kwargs): @@ -127,11 +127,11 @@ class TestHandleProviderDeletionDecorator: @patch("api.decorators.rls_transaction") @patch("api.decorators.Provider.objects.filter") def test_provider_exists_reraises_original( - self, mock_filter, mock_rls, tenants_fixture, providers_fixture + self, mock_filter, mock_rls, tenants_fixture, aws_provider ): """Re-raises original exception when provider still exists.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider mock_rls.return_value.__enter__ = lambda s: None mock_rls.return_value.__exit__ = lambda s, *args: None @@ -187,11 +187,11 @@ class TestHandleProviderDeletionDecorator: @patch("api.decorators.rls_transaction") @patch("api.decorators.Provider.objects.filter") def test_database_error_provider_exists_reraises( - self, mock_filter, mock_rls, tenants_fixture, providers_fixture + self, mock_filter, mock_rls, tenants_fixture, aws_provider ): """Re-raises original DatabaseError when provider still exists.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider mock_rls.return_value.__enter__ = lambda s: None mock_rls.return_value.__exit__ = lambda s, *args: None diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index 3ec823b89a..5095da3a0e 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -19,8 +19,8 @@ from django.db import IntegrityError @pytest.mark.django_db class TestResourceModel: - def test_setting_tags(self, providers_fixture): - provider, *_ = providers_fixture + def test_setting_tags(self, aws_provider): + provider = aws_provider tenant_id = provider.tenant_id resource = Resource.objects.create( @@ -111,9 +111,9 @@ class TestResourceModel: # @pytest.mark.django_db # class TestFindingModel: # def test_add_finding_with_long_uid( -# self, providers_fixture, scans_fixture, resources_fixture +# self, aws_provider, scans_fixture, resources_fixture # ): -# provider, *_ = providers_fixture +# provider = aws_provider # tenant_id = provider.tenant_id # long_uid = "1" * 500 @@ -372,8 +372,8 @@ class TestSAMLConfigurationModel: @pytest.mark.django_db class TestProviderComplianceScoreModel: - def test_create_provider_compliance_score(self, providers_fixture, scans_fixture): - provider = providers_fixture[0] + def test_create_provider_compliance_score(self, aws_provider, scans_fixture): + provider = aws_provider scan = scans_fixture[0] scan.completed_at = datetime.now(UTC) scan.save() @@ -393,9 +393,9 @@ class TestProviderComplianceScoreModel: assert score.requirement_status == StatusChoices.PASS def test_unique_constraint_per_provider_compliance_requirement( - self, providers_fixture, scans_fixture + self, aws_provider, scans_fixture ): - provider = providers_fixture[0] + provider = aws_provider scan = scans_fixture[0] scan.completed_at = datetime.now(UTC) scan.save() @@ -422,9 +422,9 @@ class TestProviderComplianceScoreModel: ) def test_different_providers_same_requirement_allowed( - self, providers_fixture, scans_fixture + self, aws_provider_pair, scans_fixture ): - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = scans_fixture[0] scan1.completed_at = datetime.now(UTC) scan1.save() diff --git a/api/src/backend/api/tests/test_rbac.py b/api/src/backend/api/tests/test_rbac.py index 4fab2d37b9..f2e2fc4bb5 100644 --- a/api/src/backend/api/tests/test_rbac.py +++ b/api/src/backend/api/tests/test_rbac.py @@ -434,11 +434,11 @@ class TestUserViewSet: @pytest.mark.django_db class TestProviderViewSet: def test_list_providers_with_all_permissions( - self, authenticated_client_rbac, providers_fixture + self, authenticated_client_rbac, aws_provider ): response = authenticated_client_rbac.get(reverse("provider-list")) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(providers_fixture) + assert len(response.json()["data"]) == 1 def test_list_providers_with_no_permissions( self, authenticated_client_no_permissions_rbac @@ -450,9 +450,9 @@ class TestProviderViewSet: assert len(response.json()["data"]) == 0 def test_retrieve_provider_with_all_permissions( - self, authenticated_client_rbac, providers_fixture + self, authenticated_client_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client_rbac.get( reverse("provider-detail", kwargs={"pk": provider.id}) ) @@ -460,9 +460,9 @@ class TestProviderViewSet: assert response.json()["data"]["attributes"]["alias"] == provider.alias def test_retrieve_provider_with_no_permissions( - self, authenticated_client_no_permissions_rbac, providers_fixture + self, authenticated_client_no_permissions_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client_no_permissions_rbac.get( reverse("provider-detail", kwargs={"pk": provider.id}) ) @@ -486,9 +486,9 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_403_FORBIDDEN def test_partial_update_provider_with_all_permissions( - self, authenticated_client_rbac, providers_fixture + self, authenticated_client_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider payload = { "data": { "type": "providers", @@ -505,9 +505,9 @@ class TestProviderViewSet: assert response.json()["data"]["attributes"]["alias"] == "updated_alias" def test_partial_update_provider_with_no_permissions( - self, authenticated_client_no_permissions_rbac, providers_fixture + self, authenticated_client_no_permissions_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider update_payload = { "data": { "type": "providers", @@ -528,7 +528,7 @@ class TestProviderViewSet: mock_delete_task, mock_task_get, authenticated_client_rbac, - providers_fixture, + aws_provider, tasks_fixture, ): prowler_task = tasks_fixture[0] @@ -537,7 +537,7 @@ class TestProviderViewSet: mock_delete_task.return_value = task_mock mock_task_get.return_value = prowler_task - provider1, *_ = providers_fixture + provider1 = aws_provider response = authenticated_client_rbac.delete( reverse("provider-detail", kwargs={"pk": provider1.id}) ) @@ -549,9 +549,9 @@ class TestProviderViewSet: assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}" def test_delete_provider_with_no_permissions( - self, authenticated_client_no_permissions_rbac, providers_fixture + self, authenticated_client_no_permissions_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client_no_permissions_rbac.delete( reverse("provider-detail", kwargs={"pk": provider.id}) ) @@ -564,7 +564,7 @@ class TestProviderViewSet: mock_provider_connection, mock_task_get, authenticated_client_rbac, - providers_fixture, + aws_provider, tasks_fixture, ): prowler_task = tasks_fixture[0] @@ -574,7 +574,7 @@ class TestProviderViewSet: mock_provider_connection.return_value = task_mock mock_task_get.return_value = prowler_task - provider1, *_ = providers_fixture + provider1 = aws_provider assert provider1.connected is None assert provider1.connection_last_checked_at is None @@ -589,9 +589,9 @@ class TestProviderViewSet: assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}" def test_connection_with_no_permissions( - self, authenticated_client_no_permissions_rbac, providers_fixture + self, authenticated_client_no_permissions_rbac, aws_provider ): - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client_no_permissions_rbac.post( reverse("provider-connection", kwargs={"pk": provider.id}) ) @@ -604,10 +604,10 @@ class TestLimitedVisibility: TEST_PASSWORD = "Thisisapassword123@" @pytest.fixture - def limited_admin_user(self, django_db_blocker, tenants_fixture, providers_fixture): + def limited_admin_user(self, django_db_blocker, tenants_fixture, aws_provider): with django_db_blocker.unblock(): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider user = User.objects.create_user( name="testing", email=self.TEST_EMAIL, @@ -654,25 +654,17 @@ class TestLimitedVisibility: @pytest.fixture def authenticated_client_rbac_limited( - self, limited_admin_user, tenants_fixture, client + self, + limited_admin_user, + tenants_fixture, + authenticated_client_for_tenant_factory, ): - client.user = limited_admin_user - tenant_id = tenants_fixture[0].id - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": self.TEST_EMAIL, - "password": self.TEST_PASSWORD, - "tenant_id": tenant_id, - } + return authenticated_client_for_tenant_factory( + limited_admin_user, tenants_fixture[0] ) - serializer.is_valid(raise_exception=True) - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client def test_integrations( - self, authenticated_client_rbac_limited, integrations_fixture, providers_fixture + self, authenticated_client_rbac_limited, integrations_fixture ): # Integration 2 is related to provider1 and provider 2 # This user cannot see provider 2 @@ -692,7 +684,7 @@ class TestLimitedVisibility: def test_overviews_providers( self, authenticated_client_rbac_limited, - providers_fixture, + provider_factory, ): # By default, the associated provider is the one which has the overview data response = authenticated_client_rbac_limited.get(reverse("overview-providers")) @@ -702,7 +694,7 @@ class TestLimitedVisibility: # Changing the provider visibility, no data should be returned # Only the associated provider to that group is changed - new_provider = providers_fixture[1] + new_provider = provider_factory() ProviderGroupMembership.objects.all().update(provider=new_provider) response = authenticated_client_rbac_limited.get(reverse("overview-providers")) @@ -722,7 +714,7 @@ class TestLimitedVisibility: self, endpoint_name, authenticated_client_rbac_limited, - providers_fixture, + provider_factory, ): # By default, the associated provider is the one which has the overview data response = authenticated_client_rbac_limited.get( @@ -735,7 +727,7 @@ class TestLimitedVisibility: # Changing the provider visibility, no data should be returned # Only the associated provider to that group is changed - new_provider = providers_fixture[1] + new_provider = provider_factory() ProviderGroupMembership.objects.all().update(provider=new_provider) response = authenticated_client_rbac_limited.get( @@ -750,7 +742,7 @@ class TestLimitedVisibility: def test_overviews_services( self, authenticated_client_rbac_limited, - providers_fixture, + provider_factory, ): # By default, the associated provider is the one which has the overview data response = authenticated_client_rbac_limited.get( @@ -762,7 +754,7 @@ class TestLimitedVisibility: # Changing the provider visibility, no data should be returned # Only the associated provider to that group is changed - new_provider = providers_fixture[1] + new_provider = provider_factory() ProviderGroupMembership.objects.all().update(provider=new_provider) response = authenticated_client_rbac_limited.get( diff --git a/api/src/backend/api/tests/test_sentry.py b/api/src/backend/api/tests/test_sentry.py index fb7abaffb4..082f563808 100644 --- a/api/src/backend/api/tests/test_sentry.py +++ b/api/src/backend/api/tests/test_sentry.py @@ -1,9 +1,33 @@ import logging -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +from config.settings import sentry as sentry_settings from config.settings.sentry import before_send +def test_initialize_sentry_skips_without_dsn(): + with ( + patch.object(sentry_settings.env, "str", return_value=""), + patch.object(sentry_settings.sentry_sdk, "init") as mock_init, + ): + sentry_settings.initialize_sentry() + + mock_init.assert_not_called() + + +def test_initialize_sentry_uses_configured_dsn(): + sentry_dsn = "https://fake-public-key@sentry.example.invalid/1" + + with ( + patch.object(sentry_settings.env, "str", return_value=sentry_dsn), + patch.object(sentry_settings.sentry_sdk, "init") as mock_init, + ): + sentry_settings.initialize_sentry() + + assert mock_init.call_args.kwargs["dsn"] == sentry_dsn + assert mock_init.call_args.kwargs["before_send"] is sentry_settings.before_send + + def _make_log_record(msg, level=logging.ERROR, name="test", args=None): """Build a real LogRecord so getMessage() works like in production.""" record = logging.LogRecord( diff --git a/api/src/backend/api/tests/test_utils.py b/api/src/backend/api/tests/test_utils.py index 935a15c4f3..4e7e53cb6f 100644 --- a/api/src/backend/api/tests/test_utils.py +++ b/api/src/backend/api/tests/test_utils.py @@ -188,10 +188,10 @@ class TestProwlerProviderConnectionTest: @pytest.mark.django_db @patch("api.utils.return_prowler_provider") def test_prowler_provider_connection_test_without_secret( - self, mock_return_prowler_provider, providers_fixture + self, mock_return_prowler_provider, aws_provider ): mock_return_prowler_provider.return_value = MagicMock() - connection = prowler_provider_connection_test(providers_fixture[0]) + connection = prowler_provider_connection_test(aws_provider) assert connection.is_connected is False assert isinstance(connection.error, Provider.secret.RelatedObjectDoesNotExist) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 1348d068e2..6b51261688 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -58,7 +58,6 @@ from api.models import ( ) from api.rls import Tenant from api.uuid_utils import datetime_to_uuid7 -from api.v1.serializers import TokenSerializer from api.v1.views import ( ComplianceOverviewViewSet, CustomSAMLLoginView, @@ -851,21 +850,15 @@ class TestTenantViewSet: assert response.json()["data"] == [] def test_tenants_list_memberships_as_member( - self, authenticated_client, tenants_fixture, extra_users + self, authenticated_client_for_tenant_factory, tenants_fixture, extra_users ): _, tenant2, _ = tenants_fixture _, user3_membership = extra_users user3, membership3 = user3_membership - token_response = authenticated_client.post( - reverse("token-obtain"), - data={"email": user3.email, "password": TEST_PASSWORD}, - format="json", - ) - access_token = token_response.json()["data"]["attributes"]["access"] + client = authenticated_client_for_tenant_factory(user3, tenant2) - response = authenticated_client.get( + response = client.get( reverse("tenant-membership-list", kwargs={"tenant_pk": tenant2.id}), - headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_200_OK # User is a member and can only see its own membership @@ -1428,23 +1421,29 @@ class TestMembershipViewSet: class TestProviderViewSet: @pytest.fixture(scope="function") def create_provider_group_relationship( - self, tenants_fixture, providers_fixture, provider_groups_fixture + self, tenants_fixture, aws_provider, provider_groups_fixture ): tenant, *_ = tenants_fixture - provider1, *_ = providers_fixture + provider1 = aws_provider provider_group1, *_ = provider_groups_fixture provider_group_membership = ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=provider_group1 ) return provider_group_membership - def test_providers_list(self, authenticated_client, providers_fixture): - response = authenticated_client.get(reverse("provider-list")) + def test_providers_list(self, authenticated_client, all_provider_types_fixture): + response = authenticated_client.get( + reverse("provider-list"), {"page[disable]": "true"} + ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(providers_fixture) + data = response.json()["data"] + assert len(data) == len(all_provider_types_fixture) + assert {item["attributes"]["provider"] for item in data} == { + provider.provider for provider in all_provider_types_fixture + } def test_providers_filter_provider_type( - self, authenticated_client, providers_fixture + self, authenticated_client, aws_provider_pair ): response = authenticated_client.get( reverse("provider-list"), {"filter[provider_type]": "aws"} @@ -1455,7 +1454,7 @@ class TestProviderViewSet: assert all(item["attributes"]["provider"] == "aws" for item in data) def test_providers_filter_provider_type_in( - self, authenticated_client, providers_fixture + self, authenticated_client, aws_provider_pair, gcp_provider ): response = authenticated_client.get( reverse("provider-list"), {"filter[provider_type__in]": "aws,gcp"} @@ -1466,7 +1465,7 @@ class TestProviderViewSet: assert {"aws", "gcp"} >= {item["attributes"]["provider"] for item in data} def test_providers_filter_provider_type_invalid( - self, authenticated_client, providers_fixture + self, authenticated_client, aws_provider ): response = authenticated_client.get( reverse("provider-list"), {"filter[provider_type]": "invalid"} @@ -1477,11 +1476,11 @@ class TestProviderViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider_pair, provider_groups_fixture, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -1510,7 +1509,7 @@ class TestProviderViewSet: assert len(response.json()["data"]) == 2 def test_providers_disable_pagination( - self, authenticated_client, providers_fixture, tenants_fixture + self, authenticated_client, aws_provider, tenants_fixture ): tenant, *_ = tenants_fixture existing_count = Provider.objects.filter(tenant_id=tenant.id).count() @@ -1559,13 +1558,13 @@ class TestProviderViewSet: include_values, expected_resources, authenticated_client, - providers_fixture, + aws_provider, ): response = authenticated_client.get( reverse("provider-list"), {"include": include_values} ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(providers_fixture) + assert len(response.json()["data"]) == 1 assert "included" in response.json() included_data = response.json()["included"] @@ -1574,8 +1573,8 @@ class TestProviderViewSet: f"Expected type '{expected_type}' not found in included data" ) - def test_providers_retrieve(self, authenticated_client, providers_fixture): - provider1, *_ = providers_fixture + def test_providers_retrieve(self, authenticated_client, aws_provider): + provider1 = aws_provider response = authenticated_client.get( reverse("provider-detail", kwargs={"pk": provider1.id}), ) @@ -1726,6 +1725,11 @@ class TestProviderViewSet: "uid": "C12", "alias": "Google Workspace Minimum Length", }, + { + "provider": "image", + "uid": "registry.example.com/prowler/test:latest", + "alias": "Container Image", + }, { "provider": "okta", "uid": "acme.okta.com", @@ -2316,8 +2320,8 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_201_CREATED assert Provider.objects.get().uid == stored_uid - def test_providers_partial_update(self, authenticated_client, providers_fixture): - provider1, *_ = providers_fixture + def test_providers_partial_update(self, authenticated_client, aws_provider): + provider1 = aws_provider new_alias = "This is the new name" payload = { "data": { @@ -2336,9 +2340,11 @@ class TestProviderViewSet: assert provider1.alias == new_alias def test_providers_partial_update_invalid_content_type( - self, authenticated_client, providers_fixture + self, + authenticated_client, + aws_provider, ): - provider1, *_ = providers_fixture + provider1 = aws_provider response = authenticated_client.patch( reverse("provider-detail", kwargs={"pk": provider1.id}), data={}, @@ -2346,9 +2352,11 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE def test_providers_partial_update_invalid_content( - self, authenticated_client, providers_fixture + self, + authenticated_client, + aws_provider, ): - provider1, *_ = providers_fixture + provider1 = aws_provider new_name = "This is the new name" payload = {"alias": new_name} response = authenticated_client.patch( @@ -2368,11 +2376,11 @@ class TestProviderViewSet: def test_providers_partial_update_invalid_fields( self, authenticated_client, - providers_fixture, + aws_provider, attribute_key, attribute_value, ): - provider1, *_ = providers_fixture + provider1 = aws_provider payload = { "data": { "type": "providers", @@ -2394,7 +2402,7 @@ class TestProviderViewSet: mock_delete_task, mock_task_get, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): prowler_task = tasks_fixture[0] @@ -2403,7 +2411,7 @@ class TestProviderViewSet: mock_delete_task.return_value = task_mock mock_task_get.return_value = prowler_task - provider1, *_ = providers_fixture + provider1 = aws_provider response = authenticated_client.delete( reverse("provider-detail", kwargs={"pk": provider1.id}) ) @@ -2427,7 +2435,7 @@ class TestProviderViewSet: mock_provider_connection, mock_task_get, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): prowler_task = tasks_fixture[0] @@ -2437,7 +2445,7 @@ class TestProviderViewSet: mock_provider_connection.return_value = task_mock mock_task_get.return_value = prowler_task - provider1, *_ = providers_fixture + provider1 = aws_provider assert provider1.connected is None assert provider1.connection_last_checked_at is None @@ -2452,7 +2460,8 @@ class TestProviderViewSet: assert response.headers["Content-Location"] == f"/api/v1/tasks/{task_mock.id}" def test_providers_connection_invalid_provider( - self, authenticated_client, providers_fixture + self, + authenticated_client, ): response = authenticated_client.post( reverse("provider-connection", kwargs={"pk": "random_id"}) @@ -2460,42 +2469,24 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.parametrize( - "filter_name, filter_value, expected_count", + "filter_name, filter_value", ( [ - ("provider", "aws", 2), - ("provider.in", "azure,gcp", 2), - ("uid", "123456789012", 1), - ( - "uid.icontains", - "1", - 12, - ), - ("alias", "aws_testing_1", 1), - ("alias.icontains", "aws", 2), - ("inserted_at", TODAY, 14), - ( - "inserted_at.gte", - "2024-01-01", - 14, - ), - ("inserted_at.lte", "2024-01-01", 0), - ( - "updated_at.gte", - "2024-01-01", - 14, - ), - ("updated_at.lte", "2024-01-01", 0), + ("uid", "123456789012"), + ("uid.icontains", "1"), + ("alias", "aws_testing_1"), + ("inserted_at", TODAY), + ("inserted_at.gte", "2024-01-01"), + ("updated_at.gte", "2024-01-01"), ] ), ) - def test_providers_filters( + def test_providers_filters_single_aws_provider( self, authenticated_client, - providers_fixture, + aws_provider, filter_name, filter_value, - expected_count, ): response = authenticated_client.get( reverse("provider-list"), @@ -2503,7 +2494,69 @@ class TestProviderViewSet: ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == expected_count + assert len(response.json()["data"]) == 1 + + @pytest.mark.parametrize( + "filter_name, filter_value", + ( + [ + ("inserted_at.lte", "2024-01-01"), + ("updated_at.lte", "2024-01-01"), + ] + ), + ) + def test_providers_filters_single_aws_provider_no_results( + self, + authenticated_client, + aws_provider, + filter_name, + filter_value, + ): + response = authenticated_client.get( + reverse("provider-list"), + {f"filter[{filter_name}]": filter_value}, + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 0 + + @pytest.mark.parametrize( + "filter_name, filter_value", + ( + [ + ("provider", "aws"), + ("alias.icontains", "aws"), + ] + ), + ) + def test_providers_filters_two_aws_providers( + self, + authenticated_client, + aws_provider_pair, + filter_name, + filter_value, + ): + response = authenticated_client.get( + reverse("provider-list"), + {f"filter[{filter_name}]": filter_value}, + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 2 + + def test_providers_filters_provider_in( + self, + authenticated_client, + azure_provider, + gcp_provider, + ): + response = authenticated_client.get( + reverse("provider-list"), + {"filter[provider.in]": "azure,gcp"}, + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 2 @pytest.mark.parametrize( "filter_name", @@ -2697,9 +2750,9 @@ class TestProviderGroupViewSet: assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_provider_group_create_with_relationships( - self, authenticated_client, providers_fixture, roles_fixture + self, authenticated_client, aws_provider_pair, roles_fixture ): - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair role1, role2, *_ = roles_fixture data = { @@ -2740,12 +2793,13 @@ class TestProviderGroupViewSet: self, authenticated_client, provider_groups_fixture, - providers_fixture, + gcp_provider, + kubernetes_provider, roles_fixture, ): group = provider_groups_fixture[0] - provider3 = providers_fixture[2] - provider4 = providers_fixture[3] + provider3 = gcp_provider + provider4 = kubernetes_provider role3 = roles_fixture[2] role4 = roles_fixture[3] @@ -2782,11 +2836,15 @@ class TestProviderGroupViewSet: assert set(group.roles.all()) == {role3, role4} def test_provider_group_clear_relationships( - self, authenticated_client, providers_fixture, provider_groups_fixture + self, + authenticated_client, + gcp_provider, + kubernetes_provider, + provider_groups_fixture, ): group = provider_groups_fixture[0] - provider3 = providers_fixture[2] - provider4 = providers_fixture[3] + provider3 = gcp_provider + provider4 = kubernetes_provider data = { "data": { @@ -2862,7 +2920,12 @@ class TestProviderSecretViewSet: def test_provider_secrets_list(self, authenticated_client, provider_secret_fixture): response = authenticated_client.get(reverse("providersecret-list")) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(provider_secret_fixture) + assert len(response.json()["data"]) == min( + settings.REST_FRAMEWORK["PAGE_SIZE"], len(provider_secret_fixture) + ) + assert response.json()["meta"]["pagination"]["count"] == len( + provider_secret_fixture + ) def test_provider_secrets_retrieve( self, authenticated_client, provider_secret_fixture @@ -3133,6 +3196,15 @@ current-context: test-context "api_token": "fake-vercel-api-token-for-testing", }, ), + # Image registry credentials + ( + Provider.ProviderChoices.IMAGE.value, + ProviderSecret.TypeChoices.STATIC, + { + "registry_username": "user", + "registry_password": "pass", + }, + ), # Okta with inline private key credentials ( Provider.ProviderChoices.OKTA.value, @@ -3151,16 +3223,12 @@ current-context: test-context def test_provider_secrets_create_valid( self, authenticated_client, - providers_fixture, + provider_factory, provider_type, secret_type, secret_data, ): - # Get the provider from the fixture and set its type - try: - provider = Provider.objects.filter(provider=provider_type)[0] - except IndexError: - print(f"Provider {provider_type} not found") + provider = provider_factory(provider_type) data = { "data": { @@ -3230,13 +3298,13 @@ current-context: test-context ) def test_provider_secrets_invalid_create( self, - providers_fixture, + aws_provider, authenticated_client, attributes, error_code, error_pointer, ): - provider, *_ = providers_fixture + provider = aws_provider data = { "data": { "type": "provider-secrets", @@ -3260,14 +3328,9 @@ current-context: test-context def test_provider_secrets_invalid_create_okta_missing_private_key( self, - providers_fixture, + okta_provider, authenticated_client, ): - okta_provider = next( - provider - for provider in providers_fixture - if provider.provider == Provider.ProviderChoices.OKTA.value - ) data = { "data": { "type": "provider-secrets", @@ -3391,30 +3454,43 @@ current-context: test-context ) assert response.status_code == status.HTTP_404_NOT_FOUND - @pytest.mark.parametrize( - "filter_name, filter_value, expected_count", - ( - [ - ("name", "aws_testing_1", 1), - ("name.icontains", "aws", 2), - ] - ), - ) - def test_provider_secrets_filters( + def test_provider_secrets_filter_name( self, authenticated_client, provider_secret_fixture, - filter_name, - filter_value, - expected_count, ): response = authenticated_client.get( reverse("providersecret-list"), - {f"filter[{filter_name}]": filter_value}, + {"filter[name]": "aws_testing_1"}, ) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == expected_count + assert len(response.json()["data"]) == 1 + + def test_provider_secrets_filter_name_icontains( + self, + authenticated_client, + provider_secret_fixture, + provider_factory, + ): + provider = provider_factory( + Provider.ProviderChoices.AWS.value, alias="aws_testing_extra" + ) + ProviderSecret.objects.create( + tenant_id=provider.tenant_id, + provider=provider, + secret_type=ProviderSecret.TypeChoices.STATIC, + secret={"key": "value"}, + name=provider.alias, + ) + + response = authenticated_client.get( + reverse("providersecret-list"), + {"filter[name.icontains]": "aws"}, + ) + + assert response.status_code == status.HTTP_200_OK + assert len(response.json()["data"]) == 2 @pytest.mark.parametrize( "filter_name", @@ -3552,18 +3628,9 @@ current-context: test-context assert response.status_code == status.HTTP_400_BAD_REQUEST def test_m365_provider_secrets_invalid_certificate_base64( - self, authenticated_client, providers_fixture + self, authenticated_client, m365_provider ): """Test M365 provider secret creation with invalid base64 certificate content""" - # Find M365 provider from fixture - m365_provider = None - for provider in providers_fixture: - if provider.provider == Provider.ProviderChoices.M365.value: - m365_provider = provider - break - - assert m365_provider is not None, "M365 provider not found in fixture" - data = { "data": { "type": "provider-secrets", @@ -3669,9 +3736,9 @@ class TestScanViewSet: authenticated_client, scan_json_payload, _expected_scanner_args, - providers_fixture, + okta_provider, ): - *_, provider5 = providers_fixture + provider5 = okta_provider # Provider5 has these scanner_args # scanner_args={"key1": "value1", "key2": {"key21": "value21"}} @@ -3703,12 +3770,12 @@ class TestScanViewSet: self, mock_perform_scan_task, authenticated_client, - providers_fixture, + aws_provider, tenants_fixture, django_capture_on_commit_callbacks, ): tenant, *_ = tenants_fixture - provider, *_ = providers_fixture + provider = aws_provider task_result = TaskResult.objects.create( task_id=str(uuid4()), task_name="scan-perform", @@ -3759,12 +3826,12 @@ class TestScanViewSet: self, mock_perform_scan_task, authenticated_client, - providers_fixture, + aws_provider, tenants_fixture, django_capture_on_commit_callbacks, ): tenant, *_ = tenants_fixture - provider, *_ = providers_fixture + provider = aws_provider task_result = TaskResult.objects.create( task_id=str(uuid4()), task_name="scan-perform-scheduled", @@ -3836,10 +3903,10 @@ class TestScanViewSet: self, authenticated_client, scan_json_payload, - providers_fixture, + aws_provider, error_code, ): - provider1, *_ = providers_fixture + provider1 = aws_provider scan_json_payload["data"]["relationships"]["provider"]["data"]["id"] = str( provider1.id ) @@ -4899,12 +4966,13 @@ class TestAttackPathsScanViewSet: def test_attack_paths_scans_list_returns_latest_entry_per_provider( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, + aws_provider_pair, ): - provider = providers_fixture[0] - other_provider = providers_fixture[1] + provider = aws_provider + other_provider = aws_provider_pair[1] older_scan = create_attack_paths_scan( provider, @@ -4947,13 +5015,13 @@ class TestAttackPathsScanViewSet: def test_attack_paths_scans_list_prefers_active_sink_scan_on_rollback( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, settings, ): settings.ATTACK_PATHS_SINK_DATABASE = "neo4j" - provider = providers_fixture[0] + provider = aws_provider neo4j_scan = create_attack_paths_scan( provider, @@ -4980,13 +5048,13 @@ class TestAttackPathsScanViewSet: def test_attack_paths_scans_list_falls_back_when_active_sink_has_no_scan( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, settings, ): settings.ATTACK_PATHS_SINK_DATABASE = "neptune" - provider = providers_fixture[0] + provider = aws_provider legacy_scan = create_attack_paths_scan( provider, @@ -5005,16 +5073,17 @@ class TestAttackPathsScanViewSet: def test_attack_paths_scans_list_respects_provider_group_visibility( self, authenticated_client_no_permissions_rbac, - providers_fixture, + aws_provider, create_attack_paths_scan, + aws_provider_pair, ): client = authenticated_client_no_permissions_rbac limited_user = client.user membership = Membership.objects.filter(user=limited_user).first() tenant = membership.tenant - allowed_provider = providers_fixture[0] - denied_provider = providers_fixture[1] + allowed_provider = aws_provider + denied_provider = aws_provider_pair[1] allowed_scan = create_attack_paths_scan(allowed_provider) create_attack_paths_scan(denied_provider) @@ -5045,11 +5114,11 @@ class TestAttackPathsScanViewSet: def test_attack_paths_scan_retrieve( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5088,11 +5157,11 @@ class TestAttackPathsScanViewSet: def test_attack_paths_queries_returns_catalog( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5133,11 +5202,11 @@ class TestAttackPathsScanViewSet: def test_attack_paths_queries_returns_404_when_catalog_missing( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan(provider, scan=scans_fixture[0]) with patch("api.v1.views.get_queries_for_provider", return_value=[]): @@ -5153,11 +5222,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_returns_graph( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5248,11 +5317,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_returns_text_when_accept_text_plain( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5315,11 +5384,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_blocks_when_graph_data_not_ready( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5341,11 +5410,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_allows_executing_scan_when_graph_data_ready( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5395,11 +5464,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_allows_failed_scan_when_graph_data_ready( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5449,11 +5518,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_unknown_query( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5476,11 +5545,11 @@ class TestAttackPathsScanViewSet: def test_run_attack_paths_query_returns_404_when_no_nodes_found( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5543,11 +5612,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_graph( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5600,11 +5669,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_text_when_accept_text_plain( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5653,11 +5722,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_404_when_no_nodes( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5693,11 +5762,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_400_when_graph_not_ready( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5719,11 +5788,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_403_for_write_query( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5777,12 +5846,12 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_rejects_ssrf_patterns( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, cypher, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5871,13 +5940,13 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_401_unauthenticated( self, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): from rest_framework.test import APIClient - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5898,13 +5967,13 @@ class TestAttackPathsScanViewSet: def test_cartography_schema_returns_401_unauthenticated( self, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): from rest_framework.test import APIClient - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5924,11 +5993,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_403_no_manage_scans( self, authenticated_client_no_permissions_rbac, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5951,13 +6020,13 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_does_not_leak_internals_on_error( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): from rest_framework.exceptions import APIException - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -5995,11 +6064,11 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_throttled_after_limit( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -6049,13 +6118,13 @@ class TestAttackPathsScanViewSet: def test_run_custom_query_returns_500_on_database_timeout( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): from rest_framework.exceptions import APIException - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -6090,11 +6159,11 @@ class TestAttackPathsScanViewSet: def test_cartography_schema_returns_urls( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -6140,11 +6209,11 @@ class TestAttackPathsScanViewSet: def test_cartography_schema_returns_404_when_no_metadata( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -6174,11 +6243,11 @@ class TestAttackPathsScanViewSet: def test_cartography_schema_returns_400_when_graph_not_ready( self, authenticated_client, - providers_fixture, + aws_provider, scans_fixture, create_attack_paths_scan, ): - provider = providers_fixture[0] + provider = aws_provider attack_paths_scan = create_attack_paths_scan( provider, scan=scans_fixture[0], @@ -6597,10 +6666,13 @@ class TestResourceViewSet: ) def test_resources_latest_filter_by_provider_id_in_multiple( - self, authenticated_client, providers_fixture + self, + authenticated_client, + aws_provider, + aws_provider_pair, ): """Test that provider_id__in filter works with multiple provider IDs.""" - provider1, provider2 = providers_fixture[0], providers_fixture[1] + provider1, provider2 = aws_provider, aws_provider_pair[1] tenant_id = str(provider1.tenant_id) # Create completed scans for both providers @@ -6669,12 +6741,14 @@ class TestResourceViewSet: assert len(response.json()["data"]) == 0 # Events endpoint tests - def test_events_non_aws_provider(self, authenticated_client, providers_fixture): + def test_events_non_aws_provider( + self, + authenticated_client, + azure_provider, + ): """Test events endpoint rejects non-AWS providers.""" from api.models import Resource - azure_provider = providers_fixture[4] # Azure provider from fixture - resource = Resource.objects.create( uid="test-resource-id", name="Test Resource", @@ -6710,7 +6784,7 @@ class TestResourceViewSet: def test_events_invalid_lookback_days( self, authenticated_client, - providers_fixture, + aws_provider, lookback_days, expected_status, expected_code, @@ -6719,8 +6793,6 @@ class TestResourceViewSet: """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 - resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-test", name="Test Instance", @@ -6757,7 +6829,7 @@ class TestResourceViewSet: def test_events_invalid_page_size( self, authenticated_client, - providers_fixture, + aws_provider, page_size, expected_status, expected_code, @@ -6766,8 +6838,6 @@ class TestResourceViewSet: """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", @@ -6805,15 +6875,13 @@ class TestResourceViewSet: def test_events_invalid_query_parameter( self, authenticated_client, - providers_fixture, + aws_provider, invalid_params, expected_invalid_param, ): """Test events endpoint rejects unknown query parameters 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-test", name="Test Instance", @@ -6850,13 +6918,11 @@ class TestResourceViewSet: def test_events_multiple_invalid_query_parameters( self, authenticated_client, - providers_fixture, + aws_provider, ): """Test events endpoint returns error for first unknown parameter.""" from api.models import Resource - aws_provider = providers_fixture[0] - resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-test", name="Test Instance", @@ -6893,13 +6959,11 @@ class TestResourceViewSet: mock_cloudtrail_timeline, mock_initialize_provider, authenticated_client, - providers_fixture, + aws_provider, ): """Test successful events retrieval.""" from api.models import Resource - aws_provider = providers_fixture[0] # AWS provider from fixture - # Create test resource resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-test123", @@ -6985,13 +7049,11 @@ class TestResourceViewSet: mock_cloudtrail_timeline, mock_initialize_provider, authenticated_client, - providers_fixture, + aws_provider, ): """Test events uses default lookback_days (90) when not provided.""" from api.models import Resource - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:s3:::test-bucket", name="Test Bucket", @@ -7029,13 +7091,14 @@ class TestResourceViewSet: @patch("api.v1.views.initialize_prowler_provider") def test_events_no_credentials_error( - self, mock_initialize_provider, authenticated_client, providers_fixture + self, + mock_initialize_provider, + authenticated_client, + aws_provider, ): """Test events handles missing credentials errors.""" from api.models import Resource - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:rds:us-west-2:123456789012:db:test-db", name="Test Database", @@ -7068,13 +7131,11 @@ class TestResourceViewSet: mock_cloudtrail_timeline, mock_initialize_provider, authenticated_client, - providers_fixture, + aws_provider, ): """Test events handles AccessDenied errors from AWS.""" from api.models import Resource - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:lambda:eu-west-1:123456789012:function:test-func", name="Test Function", @@ -7119,13 +7180,11 @@ class TestResourceViewSet: mock_cloudtrail_timeline, mock_initialize_provider, authenticated_client, - providers_fixture, + aws_provider, ): """Test events handles generic AWS API errors as 503.""" from api.models import Resource - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:lambda:eu-west-1:123456789012:function:test-func2", name="Test Function 2", @@ -7168,7 +7227,7 @@ class TestResourceViewSet: self, mock_initialize_provider, authenticated_client, - providers_fixture, + aws_provider, ): """Test events handles AWSAssumeRoleError during provider init. @@ -7180,8 +7239,6 @@ class TestResourceViewSet: from api.models import Resource from prowler.providers.aws.exceptions.exceptions import AWSAssumeRoleError - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:lambda:eu-west-1:123456789012:function:assume-role-test", name="AssumeRole Test Function", @@ -7225,7 +7282,7 @@ class TestResourceViewSet: assert error["status"] == "502" assert "detail" in error - def test_events_unauthenticated_returns_401(self, providers_fixture): + def test_events_unauthenticated_returns_401(self, aws_provider): """Test events endpoint returns 401 when no credentials are provided. This ensures the endpoint follows API conventions where missing authentication @@ -7234,8 +7291,6 @@ class TestResourceViewSet: from api.models import Resource from rest_framework.test import APIClient - aws_provider = providers_fixture[0] # AWS provider from fixture - resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-unauth-test", name="Test Instance", @@ -7298,7 +7353,7 @@ class TestResourceViewSet: # RLS hides resources from other tenants - should appear as not found assert response.status_code == status.HTTP_404_NOT_FOUND - def test_events_expired_token_returns_401(self, providers_fixture, tenants_fixture): + def test_events_expired_token_returns_401(self, aws_provider, tenants_fixture): """Test events endpoint returns 401 when JWT token is expired. Expired tokens should return 401 Unauthorized, not 404 Not Found. @@ -7308,8 +7363,6 @@ class TestResourceViewSet: from api.models import Resource from rest_framework.test import APIClient - aws_provider = providers_fixture[0] - resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-expired-test", name="Test Instance", @@ -7345,7 +7398,7 @@ class TestResourceViewSet: "Expired tokens should return 401, not 404." ) - def test_events_invalid_token_returns_401(self, providers_fixture): + def test_events_invalid_token_returns_401(self, aws_provider): """Test events endpoint returns 401 when JWT token is completely invalid. Malformed or invalid tokens should return 401 Unauthorized, not 404 Not Found. @@ -7353,8 +7406,6 @@ class TestResourceViewSet: from api.models import Resource from rest_framework.test import APIClient - aws_provider = providers_fixture[0] - resource = Resource.objects.create( uid="arn:aws:ec2:us-east-1:123456789012:instance/i-invalid-test", name="Test Instance", @@ -8308,11 +8359,17 @@ class TestFindingViewSet: @pytest.mark.django_db class TestJWTFields: - def test_jwt_fields(self, authenticated_client, create_test_user): - data = {"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD} - response = authenticated_client.post( - reverse("token-obtain"), data, format="json" - ) + def test_jwt_fields(self, create_test_user, tenants_fixture): + from rest_framework.test import APIClient + + client = APIClient() + data = { + "data": { + "type": "tokens", + "attributes": {"email": TEST_USER, "password": TEST_PASSWORD}, + } + } + response = client.post(reverse("token-obtain"), data, format="vnd.api+json") assert response.status_code == status.HTTP_200_OK, ( f"Unexpected status code: {response.status_code}" @@ -9532,7 +9589,7 @@ class TestUserRoleRelationshipViewSet: assert response.status_code == status.HTTP_204_NO_CONTENT def test_role_destroy_only_manage_account_blocked( - self, authenticated_client, tenants_fixture + self, authenticated_client_for_tenant_factory, tenants_fixture ): # Use a tenant without default admin role (tenant3) tenant = tenants_fixture[2] @@ -9554,24 +9611,10 @@ class TestUserRoleRelationshipViewSet: ) # Assign the role to the user UserRoleRelationship.objects.create(user=user, role=only_role, tenant=tenant) - - # Switch token to this tenant - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": TEST_USER, - "password": TEST_PASSWORD, - "tenant_id": str(tenant.id), - } - ) - serializer.is_valid(raise_exception=True) - access_token = serializer.validated_data["access"] - authenticated_client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" + client = authenticated_client_for_tenant_factory(user, tenant) # Attempt to delete the only MANAGE_ACCOUNT role - response = authenticated_client.delete( - reverse("role-detail", kwargs={"pk": only_role.id}) - ) + response = client.delete(reverse("role-detail", kwargs={"pk": only_role.id})) assert response.status_code == status.HTTP_400_BAD_REQUEST assert Role.objects.filter(id=only_role.id).exists() @@ -9728,13 +9771,16 @@ class TestRoleProviderGroupRelationshipViewSet: @pytest.mark.django_db class TestProviderGroupMembershipViewSet: def test_create_relationship( - self, authenticated_client, providers_fixture, provider_groups_fixture + self, + authenticated_client, + provider_groups_fixture, + aws_provider_pair, ): provider_group, *_ = provider_groups_fixture data = { "data": [ {"type": "provider", "id": str(provider.id)} - for provider in providers_fixture[:2] + for provider in aws_provider_pair ] } response = authenticated_client.post( @@ -9751,16 +9797,20 @@ class TestProviderGroupMembershipViewSet: ) assert relationships.count() == 2 for relationship in relationships: - assert relationship.provider.id in [p.id for p in providers_fixture[:2]] + assert relationship.provider.id in [p.id for p in aws_provider_pair] def test_create_relationship_already_exists( - self, authenticated_client, providers_fixture, provider_groups_fixture + self, + authenticated_client, + aws_provider, + provider_groups_fixture, + aws_provider_pair, ): provider_group, *_ = provider_groups_fixture data = { "data": [ {"type": "provider", "id": str(provider.id)} - for provider in providers_fixture[:2] + for provider in aws_provider_pair ] } authenticated_client.post( @@ -9774,7 +9824,7 @@ class TestProviderGroupMembershipViewSet: data = { "data": [ - {"type": "provider", "id": str(providers_fixture[0].id)}, + {"type": "provider", "id": str(aws_provider.id)}, ] } response = authenticated_client.post( @@ -9790,12 +9840,16 @@ class TestProviderGroupMembershipViewSet: assert "already associated" in errors def test_partial_update_relationship( - self, authenticated_client, providers_fixture, provider_groups_fixture + self, + authenticated_client, + provider_groups_fixture, + aws_provider_pair, + gcp_provider, ): provider_group, *_ = provider_groups_fixture data = { "data": [ - {"type": "provider", "id": str(providers_fixture[1].id)}, + {"type": "provider", "id": str(aws_provider_pair[1].id)}, ] } response = authenticated_client.patch( @@ -9811,12 +9865,12 @@ class TestProviderGroupMembershipViewSet: provider_group=provider_group.id ) assert relationships.count() == 1 - assert {rel.provider.id for rel in relationships} == {providers_fixture[1].id} + assert {rel.provider.id for rel in relationships} == {aws_provider_pair[1].id} data = { "data": [ - {"type": "provider", "id": str(providers_fixture[1].id)}, - {"type": "provider", "id": str(providers_fixture[2].id)}, + {"type": "provider", "id": str(aws_provider_pair[1].id)}, + {"type": "provider", "id": str(gcp_provider.id)}, ] } response = authenticated_client.patch( @@ -9833,18 +9887,21 @@ class TestProviderGroupMembershipViewSet: ) assert relationships.count() == 2 assert {rel.provider.id for rel in relationships} == { - providers_fixture[1].id, - providers_fixture[2].id, + aws_provider_pair[1].id, + gcp_provider.id, } def test_destroy_relationship( - self, authenticated_client, providers_fixture, provider_groups_fixture + self, + authenticated_client, + provider_groups_fixture, + aws_provider_pair, ): provider_group, *_ = provider_groups_fixture data = { "data": [ {"type": "provider", "id": str(provider.id)} - for provider in providers_fixture[:2] + for provider in aws_provider_pair ] } response = authenticated_client.post( @@ -9864,7 +9921,7 @@ class TestProviderGroupMembershipViewSet: ) assert response.status_code == status.HTTP_204_NO_CONTENT relationships = ProviderGroupMembership.objects.filter( - provider_group=providers_fixture[0].id + provider_group=provider_group.id ) assert relationships.count() == 0 @@ -9957,8 +10014,13 @@ class TestComplianceOverviewViewSet: assert response.status_code == status.HTTP_200_OK return {item["id"]: item["attributes"] for item in response.json()["data"]} - def _prepare_latest_compliance_data(self, providers_fixture): - provider1, provider2, provider3, *_ = providers_fixture + def _prepare_latest_compliance_data( + self, + aws_provider_pair, + gcp_provider, + ): + provider1, provider2 = aws_provider_pair + provider3 = gcp_provider old_scan = self._create_completed_scan(provider1, "old aws compliance scan") latest_scan1 = self._create_completed_scan( provider1, "latest aws compliance scan 1" @@ -10010,11 +10072,11 @@ class TestComplianceOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, mock_backfill_task, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="empty-compliance-scan", provider=provider, @@ -10076,11 +10138,11 @@ class TestComplianceOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, mock_backfill_task, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="preaggregated-scan", provider=provider, @@ -10156,10 +10218,13 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_id_filter_uses_latest_scan( self, authenticated_client, - providers_fixture, mock_backfill_task, + aws_provider_pair, + gcp_provider, ): - _, latest_scan, *_ = self._prepare_latest_compliance_data(providers_fixture) + _, latest_scan, *_ = self._prepare_latest_compliance_data( + aws_provider_pair, gcp_provider + ) response = authenticated_client.get( reverse("complianceoverview-list"), @@ -10175,10 +10240,11 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_id_in_filter_aggregates_latest_scans( self, authenticated_client, - providers_fixture, + aws_provider_pair, + gcp_provider, ): _, latest_scan1, latest_scan2, *_ = self._prepare_latest_compliance_data( - providers_fixture + aws_provider_pair, gcp_provider ) response = authenticated_client.get( @@ -10199,9 +10265,10 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_type_filter_uses_latest_scans( self, authenticated_client, - providers_fixture, + aws_provider_pair, + gcp_provider, ): - self._prepare_latest_compliance_data(providers_fixture) + self._prepare_latest_compliance_data(aws_provider_pair, gcp_provider) response = authenticated_client.get( reverse("complianceoverview-list"), @@ -10217,15 +10284,16 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_groups_filters_use_latest_scans( self, authenticated_client, - providers_fixture, provider_groups_fixture, tenants_fixture, + aws_provider_pair, + gcp_provider, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture _, latest_scan1, latest_scan2, *_ = self._prepare_latest_compliance_data( - providers_fixture + aws_provider_pair, gcp_provider ) ProviderGroupMembership.objects.create( tenant_id=tenant.id, @@ -10291,10 +10359,10 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_filter_returns_running_task_without_data( self, authenticated_client, - providers_fixture, + aws_provider, ): scan = self._create_completed_scan( - providers_fixture[0], "latest scan without compliance data" + aws_provider, "latest scan without compliance data" ) self._assert_latest_provider_scan_task_response( @@ -10306,9 +10374,9 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_filter_returns_running_task_for_partial_data( self, authenticated_client, - providers_fixture, + aws_provider_pair, ): - provider_with_data, provider_without_data, *_ = providers_fixture + provider_with_data, provider_without_data = aws_provider_pair scan_with_data = self._create_completed_scan( provider_with_data, "latest scan with compliance data" ) @@ -10331,10 +10399,10 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_provider_filter_empty_response_uses_scan_data_presence( self, authenticated_client, - providers_fixture, + aws_provider, ): scan = self._create_completed_scan( - providers_fixture[0], "latest scan with filtered compliance data" + aws_provider, "latest scan with filtered compliance data" ) self._create_requirement(scan, "1.1", StatusChoices.PASS, region="eu-west-1") @@ -10360,10 +10428,10 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_metadata_provider_filter_returns_running_task_without_data( self, authenticated_client, - providers_fixture, + aws_provider, ): scan = self._create_completed_scan( - providers_fixture[0], "latest scan without compliance metadata" + aws_provider, "latest scan without compliance metadata" ) self._assert_latest_provider_scan_task_response( @@ -10375,10 +10443,10 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_requirements_provider_filter_returns_running_task_without_data( self, authenticated_client, - providers_fixture, + aws_provider, ): scan = self._create_completed_scan( - providers_fixture[0], "latest scan without compliance requirements" + aws_provider, "latest scan without compliance requirements" ) self._assert_latest_provider_scan_task_response( @@ -10391,9 +10459,12 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_metadata_accepts_provider_filters( self, authenticated_client, - providers_fixture, + aws_provider_pair, + gcp_provider, ): - _, latest_scan, *_ = self._prepare_latest_compliance_data(providers_fixture) + _, latest_scan, *_ = self._prepare_latest_compliance_data( + aws_provider_pair, gcp_provider + ) response = authenticated_client.get( reverse("complianceoverview-metadata"), @@ -10407,10 +10478,11 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_requirements_accepts_provider_filters( self, authenticated_client, - providers_fixture, + aws_provider_pair, + gcp_provider, ): _, latest_scan1, latest_scan2, *_ = self._prepare_latest_compliance_data( - providers_fixture + aws_provider_pair, gcp_provider ) response = authenticated_client.get( @@ -10576,7 +10648,11 @@ class TestComplianceOverviewViewSet: assert "AWSService" in first_attr def test_compliance_overview_attributes_resolves_provider_from_scan( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + gcp_provider, + azure_provider, ): # csa_ccm_4.0 is a multi-provider universal framework: a single # compliance_id whose requirements expose different checks per provider. @@ -10585,8 +10661,6 @@ class TestComplianceOverviewViewSet: # framework and azure/gcp requirements end up with check IDs that match # no findings. tenant = tenants_fixture[0] - gcp_provider = providers_fixture[2] - azure_provider = providers_fixture[4] assert gcp_provider.provider == Provider.ProviderChoices.GCP.value assert azure_provider.provider == Provider.ProviderChoices.AZURE.value @@ -10687,7 +10761,8 @@ class TestComplianceOverviewViewSet: def test_compliance_overview_attributes_scan_scoped_by_provider_group( self, authenticated_client_no_permissions_rbac, - providers_fixture, + gcp_provider, + azure_provider, ): # A user with limited visibility (no UNLIMITED_VISIBILITY) must only be # able to resolve scans for providers in its provider groups. Tenant RLS @@ -10699,8 +10774,8 @@ class TestComplianceOverviewViewSet: membership = Membership.objects.filter(user=limited_user).first() tenant = membership.tenant - allowed_provider = providers_fixture[2] - denied_provider = providers_fixture[4] + allowed_provider = gcp_provider + denied_provider = azure_provider assert allowed_provider.provider == Provider.ProviderChoices.GCP.value assert denied_provider.provider == Provider.ProviderChoices.AZURE.value @@ -10952,11 +11027,11 @@ class TestOverviewViewSet: self, authenticated_client, resources_fixture, - providers_fixture, tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - _provider1, provider2, *_ = providers_fixture + _provider1, provider2 = aws_provider_pair scan = Scan.objects.create( name="overview scan aws account 2", @@ -11006,7 +11081,7 @@ class TestOverviewViewSet: self, authenticated_client, resources_fixture, - providers_fixture, + aws_provider, tenants_fixture, ): tenant = tenants_fixture[0] @@ -11038,14 +11113,15 @@ class TestOverviewViewSet: def test_overview_providers_count_applies_limited_visibility( self, authenticated_client_no_permissions_rbac, - providers_fixture, provider_groups_fixture, tenants_fixture, + gcp_provider, + azure_provider, ): tenant = tenants_fixture[0] client = authenticated_client_no_permissions_rbac - allowed_provider = providers_fixture[2] - denied_provider = providers_fixture[4] + allowed_provider = gcp_provider + denied_provider = azure_provider provider_group = provider_groups_fixture[0] ProviderGroupMembership.objects.create( @@ -11119,10 +11195,13 @@ class TestOverviewViewSet: ) def test_overview_threatscore_returns_weighted_aggregate_snapshot( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = self._create_scan(tenant, provider1, "agg-scan-one") scan2 = self._create_scan(tenant, provider2, "agg-scan-two") @@ -11292,10 +11371,13 @@ class TestOverviewViewSet: assert attrs["critical_requirements"] == expected_critical def test_overview_threatscore_weight_fallback_to_requirements( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = self._create_scan(tenant, provider1, "fallback-scan-1") scan2 = self._create_scan(tenant, provider2, "fallback-scan-2") @@ -11347,10 +11429,13 @@ class TestOverviewViewSet: assert aggregate["section_scores"] == {"1. IAM": "62.22"} def test_overview_threatscore_filter_by_scan_id_returns_snapshot( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider, ): tenant = tenants_fixture[0] - provider1, *_ = providers_fixture + provider1 = aws_provider scan = self._create_scan(tenant, provider1, "filter-scan") snapshot = self._create_threatscore_snapshot( @@ -11382,10 +11467,13 @@ class TestOverviewViewSet: assert body["data"][0]["attributes"]["overall_score"] == "75.00" def test_overview_threatscore_snapshot_id_returns_specific_snapshot( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider, ): tenant = tenants_fixture[0] - provider1, *_ = providers_fixture + provider1 = aws_provider scan = self._create_scan(tenant, provider1, "snapshot-id-scan") snapshot = self._create_threatscore_snapshot( @@ -11416,10 +11504,13 @@ class TestOverviewViewSet: assert data["data"]["attributes"]["score_delta"] is None def test_overview_threatscore_provider_filter_returns_unaggregated_snapshot( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = self._create_scan(tenant, provider1, "provider-filter-scan-1") scan2 = self._create_scan(tenant, provider2, "provider-filter-scan-2") @@ -11531,10 +11622,13 @@ class TestOverviewViewSet: assert service2_data["attributes"]["muted"] == 1 def test_overview_findings_provider_id_in_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="scan-one", @@ -11621,11 +11715,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, provider_groups_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -11699,10 +11793,13 @@ class TestOverviewViewSet: assert attributes["total"] == 14 def test_overview_findings_severity_provider_id_in_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="severity-scan-one", @@ -11822,10 +11919,13 @@ class TestOverviewViewSet: assert item["attributes"]["scan_ids"] == [] def test_overview_findings_severity_timeseries_with_data( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair # Create scan for day 1 scan1 = Scan.objects.create( @@ -11905,10 +12005,13 @@ class TestOverviewViewSet: assert data[2]["attributes"]["scan_ids"] == [str(scan3.id)] def test_overview_findings_severity_timeseries_aggregates_providers( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair # Same day, different providers scan1 = Scan.objects.create( @@ -11978,10 +12081,13 @@ class TestOverviewViewSet: assert set(data[0]["attributes"]["scan_ids"]) == {str(scan1.id), str(scan2.id)} def test_overview_findings_severity_timeseries_provider_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="severity-over-time-filter-scan-p1", @@ -12057,11 +12163,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, create_attack_surface_overview, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="attack-surface-scan", @@ -12105,11 +12211,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, create_attack_surface_overview, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="attack-surface-scan-1", @@ -12166,10 +12272,13 @@ class TestOverviewViewSet: assert service_ids == {"service1", "service2"} def test_overview_services_provider_type_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider, + gcp_provider, ): tenant = tenants_fixture[0] - aws_provider, _, gcp_provider, *_ = providers_fixture aws_scan = Scan.objects.create( name="aws-scan", @@ -12232,12 +12341,12 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, status_filter, _field_to_check, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="status-filter-scan", @@ -12289,10 +12398,13 @@ class TestOverviewViewSet: assert attrs["medium"] == 8 def test_overview_threatscore_compliance_id_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = self._create_scan(tenant, provider, "compliance-filter-scan") self._create_threatscore_snapshot( @@ -12341,10 +12453,13 @@ class TestOverviewViewSet: assert data[0]["attributes"]["compliance_id"] == "prowler_threatscore_aws" def test_overview_threatscore_provider_type_filter( - self, authenticated_client, tenants_fixture, providers_fixture + self, + authenticated_client, + tenants_fixture, + aws_provider, + gcp_provider, ): tenant = tenants_fixture[0] - aws_provider, _, gcp_provider, *_ = providers_fixture aws_scan = self._create_scan(tenant, aws_provider, "aws-threatscore-scan") gcp_scan = self._create_scan(tenant, gcp_provider, "gcp-threatscore-scan") @@ -12402,11 +12517,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, create_scan_category_summary, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="categories-scan", @@ -12486,16 +12601,17 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, provider_groups_fixture, create_scan_category_summary, filter_key, filter_value_fn, expected_total, expected_failed, + gcp_provider, ): tenant = tenants_fixture[0] - provider1, _, gcp_provider, *_ = providers_fixture + provider1 = aws_provider group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -12543,11 +12659,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, create_scan_category_summary, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="category-filter-scan", @@ -12580,11 +12696,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, create_scan_category_summary, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="multi-provider-scan-1", @@ -12638,11 +12754,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, create_scan_resource_group_summary, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="resource-groups-scan", @@ -12727,17 +12843,17 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, provider_groups_fixture, create_scan_resource_group_summary, filter_key, filter_value_fn, expected_total, expected_failed, + gcp_provider, ): tenant = tenants_fixture[0] - provider1 = providers_fixture[0] # AWS - gcp_provider = providers_fixture[2] # GCP + provider1 = aws_provider # AWS group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -12785,11 +12901,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, create_scan_resource_group_summary, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="rg-filter-scan", @@ -12822,11 +12938,11 @@ class TestOverviewViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, create_scan_resource_group_summary, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1 = Scan.objects.create( name="multi-provider-rg-scan-1", @@ -12900,9 +13016,9 @@ class TestOverviewViewSet: def test_compliance_watchlist_with_provider_filter_uses_provider_scores( self, authenticated_client, - providers_fixture, + aws_provider, ): - provider1 = providers_fixture[0] + provider1 = aws_provider url = f"{reverse('overview-compliance-watchlist')}?filter[provider_id]={provider1.id}" response = authenticated_client.get(url) assert response.status_code == status.HTTP_200_OK @@ -12936,9 +13052,9 @@ class TestOverviewViewSet: def test_compliance_watchlist_provider_id_in_filter( self, authenticated_client, - providers_fixture, + aws_provider_pair, ): - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair url = ( f"{reverse('overview-compliance-watchlist')}" f"?filter[provider_id__in]={provider1.id},{provider2.id}" @@ -12952,12 +13068,12 @@ class TestOverviewViewSet: def test_compliance_watchlist_provider_groups_filter( self, authenticated_client, - providers_fixture, provider_groups_fixture, tenants_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -13023,10 +13139,10 @@ class TestScheduleViewSet: mock_schedule_scan, mock_task_get, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): - provider, *_ = providers_fixture + provider = aws_provider prowler_task = tasks_fixture[0] mock_schedule_scan.return_value.id = prowler_task.id mock_task_get.return_value = prowler_task @@ -13054,10 +13170,10 @@ class TestScheduleViewSet: mock_task_get, mock_apply_async, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): - provider, *_ = providers_fixture + provider = aws_provider prowler_task = tasks_fixture[0] mock_task_get.return_value = prowler_task mock_apply_async.return_value.id = prowler_task.id @@ -13158,7 +13274,7 @@ class TestIntegrationViewSet: def test_integrations_create_valid( self, authenticated_client, - providers_fixture, + aws_provider, integration_type, configuration, credentials, @@ -13245,9 +13361,9 @@ class TestIntegrationViewSet: def test_integrations_create_valid_relationships( self, authenticated_client, - providers_fixture, + aws_provider_pair, ): - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair data = { "data": { @@ -13570,9 +13686,11 @@ class TestIntegrationViewSet: assert response.status_code == status.HTTP_400_BAD_REQUEST def test_integrations_create_duplicate_amazon_s3( - self, authenticated_client, providers_fixture + self, + authenticated_client, + aws_provider, ): - provider = providers_fixture[0] + provider = aws_provider # Create first S3 integration data = { @@ -17442,7 +17560,7 @@ class TestMuteRuleViewSet: mock_task, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): """Test that multiple findings with same UID result in only one UID in the rule.""" @@ -18441,10 +18559,13 @@ class TestFindingGroupViewSet: assert response.json()["errors"][0]["code"] == "invalid" def test_finding_groups_provider_filter( - self, authenticated_client, finding_groups_fixture, providers_fixture + self, + authenticated_client, + finding_groups_fixture, + aws_provider, ): """Test filtering by provider UUID.""" - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client.get( reverse("finding-group-list"), {"filter[inserted_at]": TODAY, "filter[provider_id]": str(provider.id)}, @@ -18472,11 +18593,11 @@ class TestFindingGroupViewSet: authenticated_client, tenants_fixture, finding_groups_fixture, - providers_fixture, provider_groups_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -18967,7 +19088,11 @@ class TestFindingGroupViewSet: # Test provider_id filter actually filters data def test_finding_groups_provider_id_filter_actually_filters( - self, authenticated_client, finding_groups_fixture, providers_fixture + self, + authenticated_client, + finding_groups_fixture, + aws_provider, + aws_provider_pair, ): """ Test that provider_id filter returns ONLY data from that provider. @@ -18975,8 +19100,8 @@ class TestFindingGroupViewSet: This is a critical test - it verifies the filter doesn't just return 200 OK, but actually restricts the data to the specified provider. """ - provider1 = providers_fixture[0] # Has scan1 with 4 checks - provider2 = providers_fixture[1] # Has scan2 with 1 check (cloudtrail_enabled) + provider1 = aws_provider # Has scan1 with 4 checks + provider2 = aws_provider_pair[1] # Has scan2 with 1 check (cloudtrail_enabled) # Get ALL finding groups (without provider filter) response_all = authenticated_client.get( @@ -19117,11 +19242,15 @@ class TestFindingGroupViewSet: assert len(data) == 0 def test_finding_groups_latest_provider_id_filter( - self, authenticated_client, finding_groups_fixture, providers_fixture + self, + authenticated_client, + finding_groups_fixture, + aws_provider, + aws_provider_pair, ): """Test /latest with provider_id filter returns only that provider's data.""" - provider1 = providers_fixture[0] # Has 4 checks - provider2 = providers_fixture[1] # Has 1 check + provider1 = aws_provider # Has 4 checks + provider2 = aws_provider_pair[1] # Has 1 check # Filter by provider1 response = authenticated_client.get( @@ -19285,8 +19414,9 @@ class TestFindingGroupViewSet: def test_finding_groups_latest_aggregates_latest_per_provider( self, authenticated_client, - providers_fixture, + aws_provider, resources_fixture, + aws_provider_pair, ): """Test /latest keeps all findings from the latest scan per provider. @@ -19294,8 +19424,8 @@ class TestFindingGroupViewSet: same check_id (e.g. one per resource), all of them are included in the aggregation — not just one. """ - provider1 = providers_fixture[0] - provider2 = providers_fixture[1] + provider1 = aws_provider + provider2 = aws_provider_pair[1] resource1 = resources_fixture[0] resource2 = resources_fixture[1] resource3 = resources_fixture[2] @@ -19420,11 +19550,11 @@ class TestFindingGroupViewSet: authenticated_client, tenants_fixture, finding_groups_fixture, - providers_fixture, provider_groups_fixture, + aws_provider_pair, ): tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair group1, group2, *_ = provider_groups_fixture ProviderGroupMembership.objects.create( tenant=tenant, provider=provider1, provider_group=group1 @@ -19902,7 +20032,7 @@ class TestFindingGroupViewSet: self, authenticated_client, tenants_fixture, - providers_fixture, + aws_provider, resources_fixture, ): """Overlapping scans on the same provider must resolve to the scan @@ -19912,7 +20042,7 @@ class TestFindingGroupViewSet: different scans and reporting diverging delta/new counts. """ tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider resource = resources_fixture[0] check_id = "overlap_regression_check" diff --git a/api/src/backend/config/settings/sentry.py b/api/src/backend/config/settings/sentry.py index 5fd6e39cc9..d75f9360e5 100644 --- a/api/src/backend/config/settings/sentry.py +++ b/api/src/backend/config/settings/sentry.py @@ -115,19 +115,27 @@ def before_send(event, hint): return event -sentry_sdk.init( - dsn=env.str("DJANGO_SENTRY_DSN", ""), - # Add data like request headers and IP for users, - # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info - before_send=before_send, - send_default_pii=True, - traces_sample_rate=env.float("DJANGO_SENTRY_TRACES_SAMPLE_RATE", default=0.02), - _experiments={ - # Set continuous_profiling_auto_start to True - # to automatically start the profiler on when - # possible. - "continuous_profiling_auto_start": True, - }, - attach_stacktrace=True, - ignore_errors=IGNORED_EXCEPTIONS, -) +def initialize_sentry(): + sentry_dsn = env.str("DJANGO_SENTRY_DSN", "") + if not sentry_dsn: + return + + sentry_sdk.init( + dsn=sentry_dsn, + # Add data like request headers and IP for users, + # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info + before_send=before_send, + send_default_pii=True, + traces_sample_rate=env.float("DJANGO_SENTRY_TRACES_SAMPLE_RATE", default=0.02), + _experiments={ + # Set continuous_profiling_auto_start to True + # to automatically start the profiler on when + # possible. + "continuous_profiling_auto_start": True, + }, + attach_stacktrace=True, + ignore_errors=IGNORED_EXCEPTIONS, + ) + + +initialize_sentry() diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index b9154ddb51..d5bf179b16 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -2,6 +2,7 @@ import logging from datetime import UTC, datetime, timedelta from types import SimpleNamespace from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest from allauth.socialaccount.models import SocialLogin @@ -50,12 +51,14 @@ from api.v1.serializers import TokenSerializer from django.conf import settings from django.db import connection as django_connection from django.db import connections as django_connections +from django.test import Client from django.urls import reverse from django_celery_results.models import TaskResult from prowler.lib.check.models import Severity from prowler.lib.outputs.finding import Status from rest_framework import status from rest_framework.test import APIClient +from rest_framework_simplejwt.tokens import AccessToken from tasks.jobs.backfill import ( aggregate_scan_category_summaries, aggregate_scan_resource_group_summaries, @@ -358,22 +361,42 @@ def create_test_user_rbac_manage_account(django_db_setup, django_db_blocker): return user +def first_membership_tenant(user): + return user.memberships.order_by("date_joined").first().tenant + + +def access_token_for_tenant(user, tenant): + access_token = AccessToken.for_user(user) + access_token["tenant_id"] = str(tenant.id) + access_token.payload["nbf"] = access_token["iat"] + return str(access_token) + + +def authenticate_client_for_tenant(client, user, tenant): + client.user = user + client.defaults["HTTP_AUTHORIZATION"] = ( + f"Bearer {access_token_for_tenant(user, tenant)}" + ) + return client + + +@pytest.fixture +def authenticated_client_for_tenant_factory(): + def create_authenticated_client(user, tenant): + return authenticate_client_for_tenant(Client(), user, tenant) + + return create_authenticated_client + + @pytest.fixture def authenticated_client_rbac_manage_account( - create_test_user_rbac_manage_account, tenants_fixture, client + create_test_user_rbac_manage_account, client ): - client.user = create_test_user_rbac_manage_account - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": "rbac_manage_account@rbac.com", - "password": TEST_PASSWORD, - } + return authenticate_client_for_tenant( + client, + create_test_user_rbac_manage_account, + first_membership_tenant(create_test_user_rbac_manage_account), ) - serializer.is_valid() - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client @pytest.fixture(scope="function") @@ -410,86 +433,43 @@ def create_test_user_rbac_manage_users_only(django_db_setup, django_db_blocker): def authenticated_client_rbac_manage_users_only( create_test_user_rbac_manage_users_only, client ): - client.user = create_test_user_rbac_manage_users_only - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": "rbac_manage_users_only@rbac.com", - "password": TEST_PASSWORD, - } + return authenticate_client_for_tenant( + client, + create_test_user_rbac_manage_users_only, + first_membership_tenant(create_test_user_rbac_manage_users_only), ) - serializer.is_valid() - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client @pytest.fixture def authenticated_client_rbac(create_test_user_rbac, tenants_fixture, client): - client.user = create_test_user_rbac - tenant_id = tenants_fixture[0].id - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": "rbac@rbac.com", - "password": TEST_PASSWORD, - "tenant_id": tenant_id, - } + return authenticate_client_for_tenant( + client, create_test_user_rbac, tenants_fixture[0] ) - serializer.is_valid(raise_exception=True) - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client @pytest.fixture def authenticated_client_rbac_noroles( create_test_user_rbac_no_roles, tenants_fixture, client ): - client.user = create_test_user_rbac_no_roles - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": "rbac_noroles@rbac.com", - "password": TEST_PASSWORD, - } + return authenticate_client_for_tenant( + client, create_test_user_rbac_no_roles, tenants_fixture[0] ) - serializer.is_valid() - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client @pytest.fixture def authenticated_client_no_permissions_rbac( create_test_user_rbac_limited, tenants_fixture, client ): - client.user = create_test_user_rbac_limited - serializer = TokenSerializer( - data={ - "type": "tokens", - "email": "rbac_limited@rbac.com", - "password": TEST_PASSWORD, - } + return authenticate_client_for_tenant( + client, create_test_user_rbac_limited, tenants_fixture[0] ) - serializer.is_valid() - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client @pytest.fixture def authenticated_client( create_test_user, tenants_fixture, set_user_admin_roles_fixture, client ): - client.user = create_test_user - serializer = TokenSerializer( - data={"type": "tokens", "email": TEST_USER, "password": TEST_PASSWORD} - ) - serializer.is_valid() - access_token = serializer.validated_data["access"] - client.defaults["HTTP_AUTHORIZATION"] = f"Bearer {access_token}" - return client + return authenticate_client_for_tenant(client, create_test_user, tenants_fixture[0]) @pytest.fixture @@ -590,109 +570,191 @@ def users_fixture(django_user_model): @pytest.fixture -def providers_fixture(tenants_fixture): - tenant, *_ = tenants_fixture - provider1 = Provider.objects.create( - provider="aws", - uid="123456789012", - alias="aws_testing_1", - tenant_id=tenant.id, - ) - provider2 = Provider.objects.create( - provider="aws", - uid="123456789013", - alias="aws_testing_2", - tenant_id=tenant.id, - ) - provider3 = Provider.objects.create( - provider="gcp", - uid="a12322-test321", - alias="gcp_testing", - tenant_id=tenant.id, - ) - provider4 = Provider.objects.create( - provider="kubernetes", - uid="kubernetes-test-12345", - alias="k8s_testing", - tenant_id=tenant.id, - ) - provider5 = Provider.objects.create( - provider="azure", - uid="37b065f8-26b0-4218-a665-0b23d07b27d9", - alias="azure_testing", - tenant_id=tenant.id, - scanner_args={"key1": "value1", "key2": {"key21": "value21"}}, - ) - provider6 = Provider.objects.create( - provider="m365", - uid="m365.test.com", - alias="m365_testing", - tenant_id=tenant.id, - ) - provider7 = Provider.objects.create( - provider="oraclecloud", - uid="ocid1.tenancy.oc1..aaaaaaaa3dwoazoox4q7wrvriywpokp5grlhgnkwtyt6dmwyou7no6mdmzda", - alias="oci_testing", - tenant_id=tenant.id, - ) - provider8 = Provider.objects.create( - provider="mongodbatlas", - uid="64b1d3c0e4b03b1234567890", - alias="mongodbatlas_testing", - tenant_id=tenant.id, - ) - provider9 = Provider.objects.create( - provider="alibabacloud", - uid="1234567890123456", - alias="alibabacloud_testing", - tenant_id=tenant.id, - ) - provider10 = Provider.objects.create( - provider="cloudflare", - uid="a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", - alias="cloudflare_testing", - tenant_id=tenant.id, - ) - provider11 = Provider.objects.create( - provider="openstack", - uid="a1b2c3d4-e5f6-7890-abcd-ef1234567890", - alias="openstack_testing", - tenant_id=tenant.id, - ) - provider12 = Provider.objects.create( - provider="googleworkspace", - uid="C12345678", - alias="googleworkspace_testing", - tenant_id=tenant.id, - ) - provider13 = Provider.objects.create( - provider="vercel", - uid="team_abcdef1234567890ab", - alias="vercel_testing", - tenant_id=tenant.id, - ) - provider14 = Provider.objects.create( - provider="okta", - uid="acme.okta.com", - alias="okta_testing", - tenant_id=tenant.id, +def provider_factory(tenants_fixture): + tenant = tenants_fixture[0] + counters = {} + + def next_counter(provider): + counters[provider] = counters.get(provider, 0) + 1 + return counters[provider] + + def defaults_for(provider, sequence): + return { + Provider.ProviderChoices.AWS.value: { + "uid": f"{123456789011 + sequence:012d}", + "alias": f"aws_testing_{sequence}", + }, + Provider.ProviderChoices.AZURE.value: { + "uid": str(uuid4()), + "alias": f"azure_testing_{sequence}", + "scanner_args": {"key1": "value1", "key2": {"key21": "value21"}}, + }, + Provider.ProviderChoices.GCP.value: { + "uid": f"a12322-test{sequence:05d}", + "alias": f"gcp_testing_{sequence}", + }, + Provider.ProviderChoices.KUBERNETES.value: { + "uid": f"kubernetes-test-{sequence}", + "alias": f"k8s_testing_{sequence}", + }, + Provider.ProviderChoices.M365.value: { + "uid": f"m365-{sequence}.test.com", + "alias": f"m365_testing_{sequence}", + }, + Provider.ProviderChoices.GITHUB.value: { + "uid": f"github-test-{sequence}", + "alias": f"github_testing_{sequence}", + }, + Provider.ProviderChoices.MONGODBATLAS.value: { + "uid": f"64b1d3c0e4b03b{sequence:010x}", + "alias": f"mongodbatlas_testing_{sequence}", + }, + Provider.ProviderChoices.IAC.value: { + "uid": f"https://github.com/prowler-cloud/test-{sequence}.git", + "alias": f"iac_testing_{sequence}", + }, + Provider.ProviderChoices.ORACLECLOUD.value: { + "uid": f"ocid1.tenancy.oc1..aaaaaaaa{sequence:024d}", + "alias": f"oci_testing_{sequence}", + }, + Provider.ProviderChoices.ALIBABACLOUD.value: { + "uid": f"{1234567890123455 + sequence:016d}", + "alias": f"alibabacloud_testing_{sequence}", + }, + Provider.ProviderChoices.CLOUDFLARE.value: { + "uid": f"{0x1000000000000000000000000000000 + sequence:032x}", + "alias": f"cloudflare_testing_{sequence}", + }, + Provider.ProviderChoices.OPENSTACK.value: { + "uid": f"openstack-project-{sequence}", + "alias": f"openstack_testing_{sequence}", + }, + Provider.ProviderChoices.IMAGE.value: { + "uid": f"registry.example.com/prowler/test:{sequence}", + "alias": f"image_testing_{sequence}", + }, + Provider.ProviderChoices.GOOGLEWORKSPACE.value: { + "uid": f"C{12345677 + sequence}", + "alias": f"googleworkspace_testing_{sequence}", + }, + Provider.ProviderChoices.VERCEL.value: { + "uid": f"team_{sequence:016x}", + "alias": f"vercel_testing_{sequence}", + }, + Provider.ProviderChoices.OKTA.value: { + "uid": f"acme-{sequence}.okta.com", + "alias": f"okta_testing_{sequence}", + }, + }[provider] + + def create_provider(provider=Provider.ProviderChoices.AWS.value, **overrides): + provider_value = getattr(provider, "value", provider) + selected_tenant = overrides.pop("tenant", tenant) + sequence = next_counter(provider_value) + attributes = { + "provider": provider_value, + "tenant_id": selected_tenant.id, + **defaults_for(provider_value, sequence), + } + attributes.update(overrides) + return Provider.objects.create(**attributes) + + return create_provider + + +@pytest.fixture +def aws_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.AWS.value) + + +@pytest.fixture +def aws_provider_pair(aws_provider, provider_factory): + return ( + aws_provider, + provider_factory(Provider.ProviderChoices.AWS.value), ) - return ( - provider1, - provider2, - provider3, - provider4, - provider5, - provider6, - provider7, - provider8, - provider9, - provider10, - provider11, - provider12, - provider13, - provider14, + +@pytest.fixture +def azure_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.AZURE.value) + + +@pytest.fixture +def gcp_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.GCP.value) + + +@pytest.fixture +def kubernetes_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.KUBERNETES.value) + + +@pytest.fixture +def m365_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.M365.value) + + +@pytest.fixture +def github_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.GITHUB.value) + + +@pytest.fixture +def mongodbatlas_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.MONGODBATLAS.value) + + +@pytest.fixture +def iac_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.IAC.value) + + +@pytest.fixture +def oraclecloud_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.ORACLECLOUD.value) + + +@pytest.fixture +def alibabacloud_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.ALIBABACLOUD.value) + + +@pytest.fixture +def cloudflare_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.CLOUDFLARE.value) + + +@pytest.fixture +def openstack_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.OPENSTACK.value) + + +@pytest.fixture +def image_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.IMAGE.value) + + +@pytest.fixture +def googleworkspace_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.GOOGLEWORKSPACE.value) + + +@pytest.fixture +def vercel_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.VERCEL.value) + + +@pytest.fixture +def okta_provider(provider_factory): + return provider_factory(Provider.ProviderChoices.OKTA.value) + + +@pytest.fixture +def all_provider_types_fixture(provider_factory): + return tuple( + provider_factory(provider_choice.value) + for provider_choice in Provider.ProviderChoices ) @@ -797,7 +859,7 @@ def roles_fixture(tenants_fixture): @pytest.fixture -def provider_secret_fixture(providers_fixture): +def provider_secret_fixture(all_provider_types_fixture): return tuple( ProviderSecret.objects.create( tenant_id=provider.tenant_id, @@ -806,14 +868,14 @@ def provider_secret_fixture(providers_fixture): secret={"key": "value"}, name=provider.alias, ) - for provider in providers_fixture + for provider in all_provider_types_fixture ) @pytest.fixture -def scans_fixture(tenants_fixture, providers_fixture): +def scans_fixture(tenants_fixture, aws_provider_pair): tenant, *_ = tenants_fixture - provider, provider2, *_ = providers_fixture + provider, provider2 = aws_provider_pair now = datetime.now(UTC) @@ -876,8 +938,8 @@ def tasks_fixture(tenants_fixture): @pytest.fixture -def resources_fixture(providers_fixture): - provider, *_ = providers_fixture +def resources_fixture(aws_provider_pair): + provider, provider2 = aws_provider_pair tags = [ ResourceTag.objects.create( @@ -918,8 +980,8 @@ def resources_fixture(providers_fixture): resource2.upsert_or_delete_tags(tags) resource3 = Resource.objects.create( - tenant_id=providers_fixture[1].tenant_id, - provider=providers_fixture[1], + tenant_id=provider2.tenant_id, + provider=provider2, uid="arn:aws:ec2:us-east-1:123456789012:bucket/i-1234567890abcdef2", name="My Bucket 3", region="us-east-1", @@ -1267,9 +1329,9 @@ def get_api_tokens( @pytest.fixture -def scan_summaries_fixture(tenants_fixture, providers_fixture): +def scan_summaries_fixture(tenants_fixture, aws_provider): tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider scan = Scan.objects.create( name="overview scan", provider=provider, @@ -1346,8 +1408,8 @@ def scan_summaries_fixture(tenants_fixture, providers_fixture): @pytest.fixture -def integrations_fixture(providers_fixture): - provider1, provider2, *_ = providers_fixture +def integrations_fixture(aws_provider_pair): + provider1, provider2 = aws_provider_pair tenant_id = provider1.tenant_id integration1 = Integration.objects.create( tenant_id=tenant_id, @@ -1408,9 +1470,9 @@ def lighthouse_config_fixture(authenticated_client, tenants_fixture): @pytest.fixture(scope="function") -def latest_scan_finding(authenticated_client, providers_fixture, resources_fixture): - provider = providers_fixture[0] - tenant_id = str(providers_fixture[0].tenant_id) +def latest_scan_finding(authenticated_client, aws_provider, resources_fixture): + provider = aws_provider + tenant_id = str(aws_provider.tenant_id) resource = resources_fixture[0] scan = Scan.objects.create( name="latest completed scan", @@ -1521,10 +1583,10 @@ def findings_with_multiple_categories(scans_fixture, resources_fixture): @pytest.fixture(scope="function") def latest_scan_finding_with_categories( - authenticated_client, providers_fixture, resources_fixture + authenticated_client, aws_provider, resources_fixture ): - provider = providers_fixture[0] - tenant_id = str(providers_fixture[0].tenant_id) + provider = aws_provider + tenant_id = str(aws_provider.tenant_id) resource = resources_fixture[0] scan = Scan.objects.create( name="latest completed scan with categories", @@ -1558,9 +1620,9 @@ def latest_scan_finding_with_categories( @pytest.fixture(scope="function") -def latest_scan_resource(authenticated_client, providers_fixture): - provider = providers_fixture[0] - tenant_id = str(providers_fixture[0].tenant_id) +def latest_scan_resource(authenticated_client, aws_provider): + provider = aws_provider + tenant_id = str(aws_provider.tenant_id) scan = Scan.objects.create( name="latest completed scan for resource", provider=provider, @@ -2025,11 +2087,11 @@ def get_authorization_header(access_token: str) -> dict: @pytest.fixture def provider_compliance_scores_fixture( - tenants_fixture, providers_fixture, scans_fixture + tenants_fixture, aws_provider_pair, scans_fixture ): """Create ProviderComplianceScore entries for compliance watchlist tests.""" tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture + provider1, provider2 = aws_provider_pair scan1, _, scan3 = scans_fixture scan1.completed_at = datetime.now(UTC) - timedelta(hours=1) @@ -2126,9 +2188,7 @@ def tenant_compliance_summary_fixture(tenants_fixture): @pytest.fixture -def finding_groups_fixture( - tenants_fixture, providers_fixture, scans_fixture, resources_fixture -): +def finding_groups_fixture(tenants_fixture, scans_fixture, resources_fixture): """ Create a comprehensive set of findings for testing Finding Groups aggregation. @@ -2147,7 +2207,6 @@ def finding_groups_fixture( - Finding counts (pass, fail, muted, new, changed) """ tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture scan1, scan2, *_ = scans_fixture resource1, resource2, *_ = resources_fixture @@ -2398,7 +2457,7 @@ def finding_groups_fixture( @pytest.fixture def finding_groups_title_variants_fixture( - tenants_fixture, providers_fixture, scans_fixture, resources_fixture + tenants_fixture, scans_fixture, resources_fixture ): """ Two providers report the same check_id with different checktitle values. @@ -2409,7 +2468,6 @@ def finding_groups_title_variants_fixture( of which title variant matches the search term. """ tenant = tenants_fixture[0] - provider1, provider2, *_ = providers_fixture scan1, scan2, *_ = scans_fixture resource1, resource2, *_ = resources_fixture diff --git a/api/src/backend/tasks/tests/report_test_helpers.py b/api/src/backend/tasks/tests/report_test_helpers.py new file mode 100644 index 0000000000..18d7b0e2b6 --- /dev/null +++ b/api/src/backend/tasks/tests/report_test_helpers.py @@ -0,0 +1,62 @@ +import io +import struct +import zlib +from types import ModuleType, SimpleNamespace +from typing import Any + +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" + + +def _png_chunk(chunk_type: bytes, data: bytes) -> bytes: + checksum = zlib.crc32(chunk_type + data) & 0xFFFFFFFF + return ( + struct.pack(">I", len(data)) + chunk_type + data + struct.pack(">I", checksum) + ) + + +def _build_tiny_png() -> bytes: + ihdr = struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0) + # Filter byte 0 plus one white RGB pixel. + idat = zlib.compress(b"\x00\xff\xff\xff") + return ( + PNG_SIGNATURE + + _png_chunk(b"IHDR", ihdr) + + _png_chunk(b"IDAT", idat) + + _png_chunk(b"IEND", b"") + ) + + +_TINY_PNG_BYTES = _build_tiny_png() + + +def fake_png_buffer() -> io.BytesIO: + return io.BytesIO(_TINY_PNG_BYTES) + + +def patch_chart_helpers( + monkeypatch: Any, module: ModuleType, names: tuple[str, ...] +) -> dict[str, list[dict[str, Any]]]: + calls: dict[str, list[dict[str, Any]]] = {name: [] for name in names} + + def _build_fake_chart(name: str): + def _fake_chart(*args: Any, **kwargs: Any) -> io.BytesIO: + calls[name].append({"args": args, "kwargs": kwargs}) + return fake_png_buffer() + + return _fake_chart + + for name in names: + monkeypatch.setattr(module, name, _build_fake_chart(name)) + + return calls + + +def patch_report_gc(monkeypatch: Any) -> None: + from tasks.jobs import report as report_module + from tasks.jobs.reports import base as base_report_module + from tasks.jobs.reports import threatscore as threatscore_report_module + + gc_stub = SimpleNamespace(collect=lambda: 0) + monkeypatch.setattr(report_module, "gc", gc_stub) + monkeypatch.setattr(base_report_module, "gc", gc_stub) + monkeypatch.setattr(threatscore_report_module, "gc", gc_stub) diff --git a/api/src/backend/tasks/tests/test_attack_paths_scan.py b/api/src/backend/tasks/tests/test_attack_paths_scan.py index ed483d33d6..d29d0980b0 100644 --- a/api/src/backend/tasks/tests/test_attack_paths_scan.py +++ b/api/src/backend/tasks/tests/test_attack_paths_scan.py @@ -106,16 +106,12 @@ class TestAttackPathsRun: mock_event_loop, mock_drop_db, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -204,16 +200,12 @@ class TestAttackPathsRun: self, mock_graph_database_preflight, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -311,16 +303,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -410,18 +398,14 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): """Failure during ingestion (before set_provider_graph_data_ready(False)) must NOT flip graph_data_ready to True for providers that never had data.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -513,16 +497,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -626,16 +606,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -739,16 +715,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -857,16 +829,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -981,16 +949,12 @@ class TestAttackPathsRun: mock_event_loop, mock_stringify, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1078,17 +1042,13 @@ class TestAttackPathsRun: @pytest.mark.django_db class TestFailAttackPathsScan: def test_marks_executing_scan_as_failed( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1120,17 +1080,13 @@ class TestFailAttackPathsScan: } def test_drops_temp_database_even_when_drop_fails( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1156,17 +1112,13 @@ class TestFailAttackPathsScan: assert attack_paths_scan.state == StateChoices.FAILED def test_skips_already_failed_scan( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1203,17 +1155,13 @@ class TestFailAttackPathsScan: fail_attack_paths_scan(str(tenant.id), "nonexistent", "setup exploded") def test_fail_recovers_graph_data_ready_when_data_exists( - self, tenants_fixture, providers_fixture, scans_fixture, sink_backend_stub + self, tenants_fixture, aws_provider, scans_fixture, sink_backend_stub ): from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1243,17 +1191,13 @@ class TestFailAttackPathsScan: mock_set_ready.assert_called_once_with(attack_paths_scan, True) def test_fail_leaves_graph_data_ready_false_when_no_data( - self, tenants_fixture, providers_fixture, scans_fixture, sink_backend_stub + self, tenants_fixture, aws_provider, scans_fixture, sink_backend_stub ): from tasks.jobs.attack_paths.db_utils import fail_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1279,17 +1223,13 @@ class TestFailAttackPathsScan: mock_set_ready.assert_not_called() def test_recover_graph_data_ready_never_raises( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import recover_graph_data_ready tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -1372,10 +1312,8 @@ class TestAttackPathsFindingsHelpers: assert mock_run_write.call_count == len(FINDINGS_INDEX_STATEMENTS) - def test_load_findings_batches_requests(self, providers_fixture): - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + def test_load_findings_batches_requests(self, aws_provider): + provider = aws_provider # Create a generator that yields two batches of dicts (pre-converted) def findings_generator(): @@ -1423,12 +1361,10 @@ class TestAttackPathsFindingsHelpers: def test_stream_findings_with_resources_returns_latest_scan_data( self, tenants_fixture, - providers_fixture, + aws_provider, ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider resource = Resource.objects.create( tenant_id=tenant.id, @@ -1527,13 +1463,11 @@ class TestAttackPathsFindingsHelpers: def test_enrich_batch_with_resources_single_resource( self, tenants_fixture, - providers_fixture, + aws_provider, ): """One finding + one resource = one output dict""" tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider resource = Resource.objects.create( tenant_id=tenant.id, @@ -1611,13 +1545,11 @@ class TestAttackPathsFindingsHelpers: def test_enrich_batch_with_resources_multiple_resources( self, tenants_fixture, - providers_fixture, + aws_provider, ): """One finding + three resources = three output dicts""" tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider resources = [] for i in range(3): @@ -1703,13 +1635,11 @@ class TestAttackPathsFindingsHelpers: def test_enrich_batch_with_resources_no_resources_skips( self, tenants_fixture, - providers_fixture, + aws_provider, ): """Finding without resources should be skipped""" tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = Scan.objects.create( name="Test Scan", @@ -1768,11 +1698,9 @@ class TestAttackPathsFindingsHelpers: assert len(result) == 0 mock_logger.warning.assert_not_called() - def test_generator_is_lazy(self, providers_fixture): + def test_generator_is_lazy(self, aws_provider): """Generator should not execute queries until iterated""" - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan_id = "some-scan-id" with patch("tasks.jobs.attack_paths.findings.rls_transaction") as mock_rls: @@ -1782,11 +1710,9 @@ class TestAttackPathsFindingsHelpers: # Nothing should be called yet mock_rls.assert_not_called() - def test_load_findings_empty_generator(self, providers_fixture): + def test_load_findings_empty_generator(self, aws_provider): """Empty generator should not call neo4j""" - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider mock_session = MagicMock() config = SimpleNamespace(update_tag=12345) @@ -2248,15 +2174,11 @@ class TestAttackPathsDbUtilsGraphDataReady: """Tests for db_utils functions related to graph_data_ready lifecycle.""" def test_database_defaults_allow_legacy_insert_without_cutover_columns( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan_id = uuid4() now = datetime.now(tz=UTC) @@ -2300,17 +2222,13 @@ class TestAttackPathsDbUtilsGraphDataReady: ) def test_create_attack_paths_scan_first_scan_defaults_to_false( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() with patch( "tasks.jobs.attack_paths.db_utils.rls_transaction", @@ -2326,17 +2244,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.sink_backend == "neo4j" def test_create_attack_paths_scan_inherits_true_from_previous( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2371,18 +2285,14 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.sink_backend == "neptune" def test_create_attack_paths_scan_prefers_active_sink_ready_scan( - self, tenants_fixture, providers_fixture, scans_fixture, settings + self, tenants_fixture, aws_provider, scans_fixture, settings ): from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan settings.ATTACK_PATHS_SINK_DATABASE = "neo4j" tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2425,17 +2335,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.sink_backend == "neo4j" def test_create_attack_paths_scan_inherits_is_migrated_false_from_legacy_ready( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() # Previous scan is ready but pre-cutover (legacy Neo4j graph shape) AttackPathsScan.objects.create( @@ -2471,17 +2377,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.sink_backend == "neo4j" def test_create_attack_paths_scan_inherits_false_when_no_previous_ready( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import create_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2514,17 +2416,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.sink_backend == "neo4j" def test_set_graph_data_ready_updates_field( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import set_graph_data_ready tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2553,17 +2451,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.graph_data_ready is True def test_finish_attack_paths_scan_does_not_modify_graph_data_ready( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2584,17 +2478,13 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.graph_data_ready is True def test_finish_attack_paths_scan_preserves_graph_data_ready_on_failure( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import finish_attack_paths_scan tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() attack_paths_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2619,18 +2509,14 @@ class TestAttackPathsDbUtilsGraphDataReady: assert attack_paths_scan.graph_data_ready is True def test_set_provider_graph_data_ready_updates_all_scans_for_provider_sink( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan_a = scans_fixture[0] - scan_a.provider = provider - scan_a.save() scan_b = Scan.objects.create( name="Second Scan", @@ -2669,18 +2555,14 @@ class TestAttackPathsDbUtilsGraphDataReady: assert new_ap_scan.graph_data_ready is False def test_set_provider_graph_data_ready_preserves_other_sink_scans( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider, scans_fixture ): from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider scan = scans_fixture[0] - scan.provider = provider - scan.save() legacy_scan = AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2711,22 +2593,14 @@ class TestAttackPathsDbUtilsGraphDataReady: assert neptune_scan.graph_data_ready is False def test_set_provider_graph_data_ready_does_not_affect_other_providers( - self, tenants_fixture, providers_fixture, scans_fixture + self, tenants_fixture, aws_provider_pair, scans_fixture ): from tasks.jobs.attack_paths.db_utils import set_provider_graph_data_ready tenant = tenants_fixture[0] - provider_a = providers_fixture[0] - provider_a.provider = Provider.ProviderChoices.AWS - provider_a.save() - - provider_b = providers_fixture[1] - provider_b.provider = Provider.ProviderChoices.AWS - provider_b.save() + provider_a, provider_b = aws_provider_pair scan_a = scans_fixture[0] - scan_a.provider = provider_a - scan_a.save() scan_b = Scan.objects.create( name="Scan for provider B", @@ -2808,15 +2682,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider # Recent scan — should still be cleaned up because worker is dead ap_scan, task_result = self._create_executing_scan( @@ -2857,15 +2729,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider old_start = datetime.now(tz=UTC) - timedelta(hours=49) ap_scan, task_result = self._create_executing_scan( @@ -2894,15 +2764,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider # Recent scan on live worker — should be skipped self._create_executing_scan(tenant, provider, worker="live-worker@host") @@ -2924,15 +2792,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider AttackPathsScan.objects.create( tenant_id=tenant.id, @@ -2966,15 +2832,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider self._create_executing_scan(tenant, provider, worker="dead-worker@host") @@ -2996,15 +2860,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant1 = tenants_fixture[0] tenant2 = tenants_fixture[1] - provider1 = providers_fixture[0] - provider1.provider = Provider.ProviderChoices.AWS - provider1.save() + provider1 = aws_provider provider2 = Provider.objects.create( provider="aws", @@ -3043,15 +2905,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider ap_scan, _ = self._create_executing_scan( tenant, provider, worker="dead-worker@host" @@ -3074,15 +2934,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider # Old scan with no Task/TaskResult old_start = datetime.now(tz=UTC) - timedelta(hours=49) @@ -3113,15 +2971,13 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider # Two scans on the same dead worker self._create_executing_scan(tenant, provider, worker="shared-worker@host") @@ -3194,14 +3050,12 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider ap_scan, task_result = self._create_scheduled_scan( tenant, @@ -3249,14 +3103,12 @@ class TestCleanupStaleAttackPathsScans: mock_drop_db, mock_recover, tenants_fixture, - providers_fixture, + aws_provider, ): from tasks.jobs.attack_paths.cleanup import cleanup_stale_attack_paths_scans tenant = tenants_fixture[0] - provider = providers_fixture[0] - provider.provider = Provider.ProviderChoices.AWS - provider.save() + provider = aws_provider ap_scan, _ = self._create_scheduled_scan( tenant, diff --git a/api/src/backend/tasks/tests/test_backfill.py b/api/src/backend/tasks/tests/test_backfill.py index 8ae39905fc..7b01c46480 100644 --- a/api/src/backend/tasks/tests/test_backfill.py +++ b/api/src/backend/tasks/tests/test_backfill.py @@ -39,9 +39,9 @@ def resource_scan_summary_data(scans_fixture): @pytest.fixture(scope="function") -def get_not_completed_scans(providers_fixture): - provider_id = providers_fixture[0].id - tenant_id = providers_fixture[0].tenant_id +def get_not_completed_scans(aws_provider): + provider_id = aws_provider.id + tenant_id = aws_provider.tenant_id scan_1 = Scan.objects.create( tenant_id=tenant_id, trigger=Scan.TriggerChoices.MANUAL, diff --git a/api/src/backend/tasks/tests/test_beat.py b/api/src/backend/tasks/tests/test_beat.py index 8679872164..e3ce97a04a 100644 --- a/api/src/backend/tasks/tests/test_beat.py +++ b/api/src/backend/tasks/tests/test_beat.py @@ -10,8 +10,8 @@ from tasks.beat import schedule_provider_scan @pytest.mark.django_db class TestScheduleProviderScan: - def test_schedule_provider_scan_success(self, providers_fixture): - provider_instance, *_ = providers_fixture + def test_schedule_provider_scan_success(self, aws_provider): + provider_instance = aws_provider with patch( "tasks.tasks.perform_scheduled_scan_task.apply_async" @@ -41,8 +41,8 @@ class TestScheduleProviderScan: "provider_id": str(provider_instance.id), } - def test_schedule_provider_scan_already_exists(self, providers_fixture): - provider_instance, *_ = providers_fixture + def test_schedule_provider_scan_already_exists(self, aws_provider): + provider_instance = aws_provider # First, schedule the scan with patch("tasks.tasks.perform_scheduled_scan_task.apply_async"): @@ -56,8 +56,8 @@ class TestScheduleProviderScan: exc_info.value ) - def test_remove_periodic_task(self, providers_fixture): - provider_instance = providers_fixture[0] + def test_remove_periodic_task(self, aws_provider): + provider_instance = aws_provider assert Scan.objects.count() == 0 with patch("tasks.tasks.perform_scheduled_scan_task.apply_async"): diff --git a/api/src/backend/tasks/tests/test_deletion.py b/api/src/backend/tasks/tests/test_deletion.py index c6e2cd408c..9a6c4acc7c 100644 --- a/api/src/backend/tasks/tests/test_deletion.py +++ b/api/src/backend/tasks/tests/test_deletion.py @@ -9,7 +9,7 @@ from tasks.jobs.deletion import delete_provider, delete_tenant @pytest.mark.django_db class TestDeleteProvider: - def test_delete_provider_success(self, providers_fixture): + def test_delete_provider_success(self, aws_provider): with ( patch( "tasks.jobs.deletion.graph_database.get_database_name", @@ -19,7 +19,7 @@ class TestDeleteProvider: "tasks.jobs.deletion.graph_database.drop_subgraph" ) as mock_drop_subgraph, ): - instance = providers_fixture[0] + instance = aws_provider tenant_id = str(instance.tenant_id) result = delete_provider(tenant_id, instance.id) @@ -53,9 +53,9 @@ class TestDeleteProvider: mock_drop_subgraph.assert_not_called() def test_delete_provider_drops_temp_attack_paths_databases( - self, providers_fixture, create_attack_paths_scan + self, aws_provider, create_attack_paths_scan ): - instance = providers_fixture[0] + instance = aws_provider tenant_id = str(instance.tenant_id) aps1 = create_attack_paths_scan(instance) @@ -84,9 +84,9 @@ class TestDeleteProvider: mock_drop_database.assert_has_calls(expected_tmp_calls, any_order=True) def test_delete_provider_drops_graph_data_from_all_recorded_sinks( - self, providers_fixture, create_attack_paths_scan + self, aws_provider, create_attack_paths_scan ): - instance = providers_fixture[0] + instance = aws_provider tenant_id = str(instance.tenant_id) create_attack_paths_scan(instance, sink_backend="neo4j") create_attack_paths_scan(instance, sink_backend="neptune") @@ -124,9 +124,9 @@ class TestDeleteProvider: ) def test_delete_provider_continues_when_temp_db_drop_fails( - self, providers_fixture, create_attack_paths_scan + self, aws_provider, create_attack_paths_scan ): - instance = providers_fixture[0] + instance = aws_provider tenant_id = str(instance.tenant_id) create_attack_paths_scan(instance) @@ -151,10 +151,10 @@ class TestDeleteProvider: def test_delete_provider_recalculates_tenant_compliance_summary( self, - providers_fixture, + aws_provider_pair, provider_compliance_scores_fixture, ): - instance = providers_fixture[0] + instance = aws_provider_pair[0] tenant_id = instance.tenant_id TenantComplianceSummary.objects.create( @@ -199,7 +199,7 @@ class TestDeleteProvider: @pytest.mark.django_db class TestDeleteTenant: - def test_delete_tenant_success(self, tenants_fixture, providers_fixture): + def test_delete_tenant_success(self, tenants_fixture, aws_provider): """ Test successful deletion of a tenant and its related data. """ diff --git a/api/src/backend/tasks/tests/test_reports.py b/api/src/backend/tasks/tests/test_reports.py index 5d156f9d81..626237922f 100644 --- a/api/src/backend/tasks/tests/test_reports.py +++ b/api/src/backend/tasks/tests/test_reports.py @@ -45,14 +45,48 @@ from tasks.jobs.reports import ( get_color_for_risk_level, get_color_for_weight, ) +from tasks.jobs.reports import cis as cis_report_module +from tasks.jobs.reports import csa as csa_report_module +from tasks.jobs.reports import ens as ens_report_module +from tasks.jobs.reports import nis2 as nis2_report_module +from tasks.jobs.reports import threatscore as threatscore_report_module from tasks.jobs.threatscore_utils import ( _aggregate_requirement_statistics_from_database, _load_findings_for_requirement_checks, ) +from tasks.tests.report_test_helpers import patch_chart_helpers, patch_report_gc matplotlib.use("Agg") # Use non-interactive backend for tests +@pytest.fixture +def patch_report_rendering(monkeypatch): + patch_report_gc(monkeypatch) + patch_chart_helpers( + monkeypatch, + cis_report_module, + ( + "create_pie_chart", + "create_horizontal_bar_chart", + "create_stacked_bar_chart", + ), + ) + patch_chart_helpers( + monkeypatch, csa_report_module, ("create_horizontal_bar_chart",) + ) + patch_chart_helpers( + monkeypatch, + ens_report_module, + ("create_horizontal_bar_chart", "create_radar_chart"), + ) + patch_chart_helpers( + monkeypatch, nis2_report_module, ("create_horizontal_bar_chart",) + ) + patch_chart_helpers( + monkeypatch, threatscore_report_module, ("create_vertical_bar_chart",) + ) + + @pytest.mark.django_db class TestAggregateRequirementStatistics: """Test suite for _aggregate_requirement_statistics_from_database function.""" @@ -355,7 +389,7 @@ class TestPDFStylesCreation: class TestLoadFindingsForChecks: """Test suite for _load_findings_for_requirement_checks function.""" - def test_empty_check_ids_returns_empty(self, tenants_fixture, providers_fixture): + def test_empty_check_ids_returns_empty(self, tenants_fixture): """Test that empty check_ids list returns empty dict.""" tenant = tenants_fixture[0] @@ -1041,6 +1075,7 @@ class TestStaleCleanupProtectionHelpers: @pytest.mark.django_db +@pytest.mark.usefixtures("patch_report_rendering") class TestGenerateThreatscoreReportFunction: """Test suite for generate_threatscore_report function.""" @@ -1050,12 +1085,12 @@ class TestGenerateThreatscoreReportFunction: mock_build_provider_metadata, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test that exceptions during report generation are properly handled.""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider mock_build_provider_metadata.side_effect = Exception("Test exception") @@ -1072,6 +1107,7 @@ class TestGenerateThreatscoreReportFunction: @pytest.mark.django_db +@pytest.mark.usefixtures("patch_report_rendering") class TestGenerateComplianceReportsOptimized: """Test suite for generate_compliance_reports function.""" @@ -1087,12 +1123,12 @@ class TestGenerateComplianceReportsOptimized: mock_upload, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test that function returns early when scan has no findings.""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider result = generate_compliance_reports( tenant_id=str(tenant.id), @@ -1144,14 +1180,14 @@ class TestGenerateComplianceReportsOptimized: mock_upload, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Scan with no findings and ``generate_cis=True`` must yield a flat ``{"upload": False, "path": ""}`` entry, consistent with the other frameworks (no nested dict, no sentinel keys).""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider result = generate_compliance_reports( tenant_id=str(tenant.id), @@ -1439,6 +1475,7 @@ class TestGenerateComplianceReportsOptimized: @pytest.mark.django_db +@pytest.mark.usefixtures("patch_report_rendering") class TestGenerateComplianceReportsCIS: """Test suite covering the CIS branch of generate_compliance_reports.""" @@ -1468,7 +1505,7 @@ class TestGenerateComplianceReportsCIS: monkeypatch, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """CIS branch should generate a single PDF for the highest version. @@ -1478,7 +1515,7 @@ class TestGenerateComplianceReportsCIS: """ tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._force_scan_has_findings(monkeypatch) @@ -1527,12 +1564,12 @@ class TestGenerateComplianceReportsCIS: monkeypatch, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """A failure in the latest CIS variant must be surfaced in the flat results entry.""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._force_scan_has_findings(monkeypatch) @@ -1574,14 +1611,14 @@ class TestGenerateComplianceReportsCIS: monkeypatch, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """When ``Compliance.get_bulk`` returns no CIS entry the CIS branch must skip cleanly and record a flat ``{"upload": False, "path": ""}`` entry — no hard-coded provider whitelist is consulted.""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._force_scan_has_findings(monkeypatch) mock_stats.return_value = {} @@ -1613,12 +1650,12 @@ class TestGenerateComplianceReportsCIS: monkeypatch, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """CIS output dir errors must be captured in results (not raised).""" tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._force_scan_has_findings(monkeypatch) mock_stats.return_value = {} diff --git a/api/src/backend/tasks/tests/test_reports_base.py b/api/src/backend/tasks/tests/test_reports_base.py index 6246654436..7577903bae 100644 --- a/api/src/backend/tasks/tests/test_reports_base.py +++ b/api/src/backend/tasks/tests/test_reports_base.py @@ -43,6 +43,7 @@ from tasks.jobs.reports import ( # Configuration; Colors; Components; Charts; B get_framework_config, get_status_color, ) +from tasks.tests.report_test_helpers import PNG_SIGNATURE, fake_png_buffer # ============================================================================= # Configuration Tests @@ -452,174 +453,47 @@ class TestSectionHeader: # ============================================================================= -class TestChartCreation: - """Tests for chart creation functions.""" +class TestChartRenderingSmoke: + """Small real-render coverage for the chart helpers.""" - def test_create_vertical_bar_chart(self): - """Test vertical bar chart creation.""" - buffer = create_vertical_bar_chart( - labels=["A", "B", "C"], - values=[80, 60, 40], - ) - assert isinstance(buffer, io.BytesIO) - assert buffer.getvalue() # Not empty + @pytest.mark.parametrize( + ("chart_helper", "kwargs"), + [ + ( + create_vertical_bar_chart, + {"labels": ["Section 1", "Section 2"], "values": [90, 70]}, + ), + ( + create_horizontal_bar_chart, + {"labels": ["Category 1", "Category 2"], "values": [85, 65]}, + ), + ( + create_radar_chart, + {"labels": ["A", "B", "C"], "values": [50, 60, 70]}, + ), + ( + create_pie_chart, + {"labels": ["Pass", "Fail"], "values": [80, 20]}, + ), + ( + create_stacked_bar_chart, + { + "labels": ["Section 1", "Section 2"], + "data_series": {"Pass": [8, 6], "Fail": [2, 4]}, + }, + ), + ], + ) + def test_chart_helper_renders_valid_png(self, chart_helper, kwargs): + buffer = chart_helper(**kwargs) + image_bytes = buffer.getvalue() - def test_create_vertical_bar_chart_with_options(self): - """Test vertical bar chart with custom options.""" - buffer = create_vertical_bar_chart( - labels=["Section 1", "Section 2"], - values=[90, 70], - ylabel="Compliance", - title="Test Chart", - figsize=(8, 6), - ) assert isinstance(buffer, io.BytesIO) + assert image_bytes + assert image_bytes.startswith(PNG_SIGNATURE) - def test_create_horizontal_bar_chart(self): - """Test horizontal bar chart creation.""" - buffer = create_horizontal_bar_chart( - labels=["Category 1", "Category 2", "Category 3"], - values=[85, 65, 45], - ) - assert isinstance(buffer, io.BytesIO) - assert buffer.getvalue() - - def test_create_horizontal_bar_chart_with_options(self): - """Test horizontal bar chart with custom options.""" - buffer = create_horizontal_bar_chart( - labels=["A", "B"], - values=[100, 50], - xlabel="Percentage", - title="Custom Chart", - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_radar_chart(self): - """Test radar chart creation.""" - buffer = create_radar_chart( - labels=["Dim 1", "Dim 2", "Dim 3", "Dim 4", "Dim 5"], - values=[80, 70, 60, 90, 75], - ) - assert isinstance(buffer, io.BytesIO) - assert buffer.getvalue() - - def test_create_radar_chart_with_options(self): - """Test radar chart with custom options.""" - buffer = create_radar_chart( - labels=["A", "B", "C"], - values=[50, 60, 70], - color="#FF0000", - fill_alpha=0.5, - title="Custom Radar", - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_pie_chart(self): - """Test pie chart creation.""" - buffer = create_pie_chart( - labels=["Pass", "Fail"], - values=[80, 20], - ) - assert isinstance(buffer, io.BytesIO) - assert buffer.getvalue() - - def test_create_pie_chart_with_options(self): - """Test pie chart with custom options.""" - buffer = create_pie_chart( - labels=["Pass", "Fail", "Manual"], - values=[60, 30, 10], - colors=["#4CAF50", "#F44336", "#9E9E9E"], - title="Status Distribution", - autopct="%1.0f%%", - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_stacked_bar_chart(self): - """Test stacked bar chart creation.""" - buffer = create_stacked_bar_chart( - labels=["Section 1", "Section 2", "Section 3"], - data_series={ - "Pass": [8, 6, 4], - "Fail": [2, 4, 6], - }, - ) - assert isinstance(buffer, io.BytesIO) - assert buffer.getvalue() - - def test_create_stacked_bar_chart_with_options(self): - """Test stacked bar chart with custom options.""" - buffer = create_stacked_bar_chart( - labels=["A", "B"], - data_series={ - "Pass": [10, 5], - "Fail": [2, 3], - "Manual": [1, 2], - }, - colors={ - "Pass": "#4CAF50", - "Fail": "#F44336", - "Manual": "#9E9E9E", - }, - xlabel="Categories", - ylabel="Requirements", - title="Requirements by Status", - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_stacked_bar_chart_without_legend(self): - """Test stacked bar chart without legend.""" - buffer = create_stacked_bar_chart( - labels=["X", "Y"], - data_series={"A": [1, 2]}, - show_legend=False, - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_vertical_bar_chart_without_labels(self): - """Test vertical bar chart without value labels.""" - buffer = create_vertical_bar_chart( - labels=["A", "B"], - values=[50, 75], - show_labels=False, - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_vertical_bar_chart_with_explicit_colors(self): - """Test vertical bar chart with explicit color list.""" - buffer = create_vertical_bar_chart( - labels=["Pass", "Fail"], - values=[80, 20], - colors=["#4CAF50", "#F44336"], - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_horizontal_bar_chart_auto_figsize(self): - """Test horizontal bar chart auto-calculates figure size for many items.""" - labels = [f"Item {i}" for i in range(20)] - values = [50 + i * 2 for i in range(20)] - buffer = create_horizontal_bar_chart( - labels=labels, - values=values, - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_horizontal_bar_chart_with_explicit_colors(self): - """Test horizontal bar chart with explicit colors.""" - buffer = create_horizontal_bar_chart( - labels=["A", "B", "C"], - values=[80, 60, 40], - colors=["#4CAF50", "#FFEB3B", "#F44336"], - ) - assert isinstance(buffer, io.BytesIO) - - def test_create_radar_chart_with_custom_ticks(self): - """Test radar chart with custom y-axis ticks.""" - buffer = create_radar_chart( - labels=["A", "B", "C", "D"], - values=[25, 50, 75, 100], - y_ticks=[0, 25, 50, 75, 100], - ) - assert isinstance(buffer, io.BytesIO) + buffer.seek(0) + assert Image(buffer, width=1 * inch, height=1 * inch) # ============================================================================= @@ -1056,10 +930,7 @@ class TestExampleReportGenerator: ] def create_charts_section(self, data): - chart_buffer = create_vertical_bar_chart( - labels=["Pass", "Fail"], - values=[80, 20], - ) + chart_buffer = fake_png_buffer() return [Image(chart_buffer, width=6 * inch, height=4 * inch)] def create_requirements_index(self, data): @@ -1150,63 +1021,6 @@ class TestExampleReportGenerator: # ============================================================================= -class TestChartEdgeCases: - """Tests for chart edge cases.""" - - def test_vertical_bar_chart_empty_data(self): - """Test vertical bar chart with empty data.""" - buffer = create_vertical_bar_chart(labels=[], values=[]) - assert isinstance(buffer, io.BytesIO) - - def test_vertical_bar_chart_single_item(self): - """Test vertical bar chart with single item.""" - buffer = create_vertical_bar_chart(labels=["Single"], values=[75.0]) - assert isinstance(buffer, io.BytesIO) - - def test_horizontal_bar_chart_empty_data(self): - """Test horizontal bar chart with empty data.""" - buffer = create_horizontal_bar_chart(labels=[], values=[]) - assert isinstance(buffer, io.BytesIO) - - def test_horizontal_bar_chart_single_item(self): - """Test horizontal bar chart with single item.""" - buffer = create_horizontal_bar_chart(labels=["Single"], values=[50.0]) - assert isinstance(buffer, io.BytesIO) - - def test_radar_chart_minimum_points(self): - """Test radar chart with minimum number of points (3).""" - buffer = create_radar_chart( - labels=["A", "B", "C"], - values=[30.0, 60.0, 90.0], - ) - assert isinstance(buffer, io.BytesIO) - - def test_pie_chart_single_slice(self): - """Test pie chart with single slice.""" - buffer = create_pie_chart(labels=["Only"], values=[100.0]) - assert isinstance(buffer, io.BytesIO) - - def test_pie_chart_many_slices(self): - """Test pie chart with many slices.""" - labels = [f"Item {i}" for i in range(10)] - values = [10.0] * 10 - buffer = create_pie_chart(labels=labels, values=values) - assert isinstance(buffer, io.BytesIO) - - def test_stacked_bar_chart_single_series(self): - """Test stacked bar chart with single series.""" - buffer = create_stacked_bar_chart( - labels=["A", "B"], - data_series={"Only": [10.0, 20.0]}, - ) - assert isinstance(buffer, io.BytesIO) - - def test_stacked_bar_chart_empty_data(self): - """Test stacked bar chart with empty data.""" - buffer = create_stacked_bar_chart(labels=[], data_series={}) - assert isinstance(buffer, io.BytesIO) - - class TestComponentEdgeCases: """Tests for component edge cases.""" diff --git a/api/src/backend/tasks/tests/test_reports_cis.py b/api/src/backend/tasks/tests/test_reports_cis.py index 31e5a5495f..b57780b8a4 100644 --- a/api/src/backend/tasks/tests/test_reports_cis.py +++ b/api/src/backend/tasks/tests/test_reports_cis.py @@ -4,11 +4,13 @@ import pytest from api.models import StatusChoices from reportlab.platypus import Image, LongTable, Paragraph, Table from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports import cis as cis_report_module from tasks.jobs.reports.cis import ( CISReportGenerator, _normalize_profile, _profile_badge_text, ) +from tasks.tests.report_test_helpers import patch_chart_helpers # ============================================================================= # Fixtures @@ -399,18 +401,69 @@ class TestCISExecutiveSummary: class TestCISChartsSection: - def test_charts_rendered(self, cis_generator, populated_cis_compliance_data): - elements = cis_generator.create_charts_section(populated_cis_compliance_data) - # At least 1 image for the pie + 1 for section bar + 1 for stacked - images = [e for e in elements if isinstance(e, Image)] - assert len(images) >= 1 + def test_charts_rendered( + self, monkeypatch, cis_generator, populated_cis_compliance_data + ): + chart_calls = patch_chart_helpers( + monkeypatch, + cis_report_module, + ( + "create_pie_chart", + "create_horizontal_bar_chart", + "create_stacked_bar_chart", + ), + ) - def test_charts_no_data_no_crash(self, cis_generator, basic_cis_compliance_data): + elements = cis_generator.create_charts_section(populated_cis_compliance_data) + + images = [e for e in elements if isinstance(e, Image)] + assert len(images) == 3 + + pie_kwargs = chart_calls["create_pie_chart"][0]["kwargs"] + assert pie_kwargs["labels"] == ["Pass (2)", "Fail (2)", "Manual (1)"] + assert pie_kwargs["values"] == [2, 2, 1] + assert pie_kwargs["colors"] + + bar_kwargs = chart_calls["create_horizontal_bar_chart"][0]["kwargs"] + assert set(bar_kwargs["labels"]) == { + "1 Identity and Access Management", + "2 Storage", + } + assert bar_kwargs["values"] == [50.0, 50.0] + assert bar_kwargs["xlabel"] == "Compliance (%)" + assert bar_kwargs["color_func"] + assert bar_kwargs["label_fontsize"] == 9 + + stacked_kwargs = chart_calls["create_stacked_bar_chart"][0]["kwargs"] + assert stacked_kwargs["labels"] == ["Level 1", "Level 2"] + assert stacked_kwargs["data_series"] == { + "Pass": [1, 1], + "Fail": [2, 0], + "Manual": [0, 1], + } + assert stacked_kwargs["xlabel"] == "Profile" + assert stacked_kwargs["ylabel"] == "Requirements" + + def test_charts_no_data_no_crash( + self, monkeypatch, cis_generator, basic_cis_compliance_data + ): + chart_calls = patch_chart_helpers( + monkeypatch, + cis_report_module, + ( + "create_pie_chart", + "create_horizontal_bar_chart", + "create_stacked_bar_chart", + ), + ) basic_cis_compliance_data.requirements = [] basic_cis_compliance_data.attributes_by_requirement_id = {} elements = cis_generator.create_charts_section(basic_cis_compliance_data) - # Must not raise; may or may not have any Image + assert isinstance(elements, list) + assert chart_calls["create_pie_chart"] == [] + assert chart_calls["create_horizontal_bar_chart"] == [] + assert chart_calls["create_stacked_bar_chart"] == [] # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_reports_csa.py b/api/src/backend/tasks/tests/test_reports_csa.py index 2e61e9ef84..7a97018f6c 100644 --- a/api/src/backend/tasks/tests/test_reports_csa.py +++ b/api/src/backend/tasks/tests/test_reports_csa.py @@ -2,9 +2,11 @@ import io from unittest.mock import Mock import pytest -from reportlab.platypus import PageBreak, Paragraph, Table +from reportlab.platypus import Image, PageBreak, Paragraph, Table from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports import csa as csa_report_module from tasks.jobs.reports.csa import CSAReportGenerator +from tasks.tests.report_test_helpers import patch_chart_helpers # Use string status values directly to avoid Django DB initialization @@ -29,6 +31,13 @@ def csa_generator(): return CSAReportGenerator(config) +@pytest.fixture +def patched_csa_charts(monkeypatch): + return patch_chart_helpers( + monkeypatch, csa_report_module, ("create_horizontal_bar_chart",) + ) + + @pytest.fixture def mock_csa_requirement_attribute_iam(): """Create a mock CSA CCM requirement attribute for Identity & Access Management.""" @@ -320,7 +329,7 @@ class TestCSAChartsSection: """Test suite for CSA charts section generation.""" def test_charts_section_has_section_chart_title( - self, csa_generator, basic_csa_compliance_data + self, csa_generator, basic_csa_compliance_data, patched_csa_charts ): """Test that charts section has section compliance title.""" basic_csa_compliance_data.requirements = [] @@ -331,9 +340,14 @@ class TestCSAChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Section" in content or "Compliance" in content + assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_csa_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == [] + assert chart_kwargs["values"] == [] + assert chart_kwargs["xlabel"] == "Compliance (%)" def test_charts_section_has_page_break( - self, csa_generator, basic_csa_compliance_data + self, csa_generator, basic_csa_compliance_data, patched_csa_charts ): """Test that charts section has page breaks.""" basic_csa_compliance_data.requirements = [] @@ -343,12 +357,14 @@ class TestCSAChartsSection: page_breaks = [e for e in elements if isinstance(e, PageBreak)] assert len(page_breaks) >= 1 + assert len(patched_csa_charts["create_horizontal_bar_chart"]) == 1 def test_charts_section_has_section_breakdown( self, csa_generator, basic_csa_compliance_data, mock_csa_requirement_attribute_iam, + patched_csa_charts, ): """Test that charts section includes section breakdown table.""" basic_csa_compliance_data.requirements = [ @@ -372,6 +388,11 @@ class TestCSAChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Section" in content or "Breakdown" in content + assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_csa_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Identity & Access Management"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["color_func"] # ============================================================================= @@ -387,6 +408,7 @@ class TestCSASectionChart: csa_generator, basic_csa_compliance_data, mock_csa_requirement_attribute_iam, + patched_csa_charts, ): """Test that section chart is created successfully.""" basic_csa_compliance_data.requirements = [ @@ -409,12 +431,17 @@ class TestCSASectionChart: assert isinstance(chart_buffer, io.BytesIO) assert chart_buffer.getvalue() # Not empty + chart_kwargs = patched_csa_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Identity & Access Management"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["xlabel"] == "Compliance (%)" def test_section_chart_excludes_manual( self, csa_generator, basic_csa_compliance_data, mock_csa_requirement_attribute_iam, + patched_csa_charts, ): """Test that manual requirements are excluded from section chart.""" basic_csa_compliance_data.requirements = [ @@ -447,6 +474,9 @@ class TestCSASectionChart: # Should not raise any errors chart_buffer = csa_generator._create_section_chart(basic_csa_compliance_data) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_csa_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Identity & Access Management"] + assert chart_kwargs["values"] == [100.0] def test_section_chart_multiple_sections( self, @@ -455,6 +485,7 @@ class TestCSASectionChart: mock_csa_requirement_attribute_iam, mock_csa_requirement_attribute_logging, mock_csa_requirement_attribute_crypto, + patched_csa_charts, ): """Test section chart with multiple sections.""" basic_csa_compliance_data.requirements = [ @@ -501,6 +532,13 @@ class TestCSASectionChart: chart_buffer = csa_generator._create_section_chart(basic_csa_compliance_data) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_csa_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == [ + "Cryptography & Encryption", + "Identity & Access Management", + "Logging and Monitoring", + ] + assert chart_kwargs["values"] == [100.0, 100.0, 0.0] # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_reports_ens.py b/api/src/backend/tasks/tests/test_reports_ens.py index 91eb6d6f3a..220c3e1427 100644 --- a/api/src/backend/tasks/tests/test_reports_ens.py +++ b/api/src/backend/tasks/tests/test_reports_ens.py @@ -2,9 +2,11 @@ import io from unittest.mock import Mock, patch import pytest -from reportlab.platypus import PageBreak, Paragraph, Table +from reportlab.platypus import Image, PageBreak, Paragraph, Table from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports import ens as ens_report_module from tasks.jobs.reports.ens import ENSReportGenerator +from tasks.tests.report_test_helpers import patch_chart_helpers # Use string status values directly to avoid Django DB initialization @@ -29,6 +31,15 @@ def ens_generator(): return ENSReportGenerator(config) +@pytest.fixture +def patched_ens_charts(monkeypatch): + return patch_chart_helpers( + monkeypatch, + ens_report_module, + ("create_horizontal_bar_chart", "create_radar_chart"), + ) + + @pytest.fixture def mock_ens_requirement_attribute(): """Create a mock ENS requirement attribute with all fields.""" @@ -355,7 +366,7 @@ class TestENSChartsSection: """Test suite for ENS charts section generation.""" def test_charts_section_has_page_breaks( - self, ens_generator, basic_ens_compliance_data + self, ens_generator, basic_ens_compliance_data, patched_ens_charts ): """Test that charts section has page breaks between charts.""" basic_ens_compliance_data.requirements = [] @@ -365,9 +376,25 @@ class TestENSChartsSection: page_breaks = [e for e in elements if isinstance(e, PageBreak)] assert len(page_breaks) >= 2 # At least 2 page breaks for different charts + assert any(isinstance(e, Image) for e in elements) + assert len(patched_ens_charts["create_horizontal_bar_chart"]) == 1 + assert len(patched_ens_charts["create_radar_chart"]) == 1 + + marco_kwargs = patched_ens_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert marco_kwargs["labels"] == [] + assert marco_kwargs["values"] == [] + + radar_kwargs = patched_ens_charts["create_radar_chart"][0]["kwargs"] + assert radar_kwargs["labels"] == ens_report_module.DIMENSION_NAMES + assert radar_kwargs["values"] == [100, 100, 100, 100, 100] + assert radar_kwargs["color"] == "#2196F3" def test_charts_section_has_marco_category_chart( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test that charts section contains Marco/Categoría chart.""" basic_ens_compliance_data.requirements = [ @@ -391,9 +418,18 @@ class TestENSChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Marco" in content or "Categoría" in content + assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_ens_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Operacional - Gestión de incidentes"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["xlabel"] == "Porcentaje de Cumplimiento (%)" def test_charts_section_has_dimensions_radar( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test that charts section contains dimensions radar chart.""" basic_ens_compliance_data.requirements = [ @@ -417,9 +453,17 @@ class TestENSChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Dimensiones" in content or "dimensiones" in content.lower() + radar_kwargs = patched_ens_charts["create_radar_chart"][0]["kwargs"] + assert radar_kwargs["labels"] == ens_report_module.DIMENSION_NAMES + assert radar_kwargs["values"] == [100, 100, 100.0, 100.0, 100] + assert radar_kwargs["color"] == "#2196F3" def test_charts_section_has_tipo_distribution( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test that charts section contains tipo distribution.""" basic_ens_compliance_data.requirements = [ @@ -443,6 +487,8 @@ class TestENSChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Tipo" in content or "tipo" in content.lower() + assert len(patched_ens_charts["create_horizontal_bar_chart"]) == 1 + assert len(patched_ens_charts["create_radar_chart"]) == 1 # ============================================================================= @@ -829,7 +875,11 @@ class TestENSDimensionHandling: """Test suite for ENS security dimension handling.""" def test_dimensions_as_list( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test handling dimensions as a list.""" # mock_ens_requirement_attribute has Dimensiones as list @@ -837,9 +887,9 @@ class TestENSDimensionHandling: RequirementData( id="REQ-001", description="Test requirement", - status=StatusChoices.PASS, - passed_findings=10, - failed_findings=0, + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, total_findings=10, ), ] @@ -854,12 +904,16 @@ class TestENSDimensionHandling: basic_ens_compliance_data ) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_ens_charts["create_radar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ens_report_module.DIMENSION_NAMES + assert chart_kwargs["values"] == [100, 100, 0.0, 0.0, 100] def test_dimensions_as_string( self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute_medio, + patched_ens_charts, ): """Test handling dimensions as comma-separated string.""" # mock_ens_requirement_attribute_medio has Dimensiones as string @@ -867,9 +921,9 @@ class TestENSDimensionHandling: RequirementData( id="REQ-001", description="Test requirement", - status=StatusChoices.PASS, - passed_findings=10, - failed_findings=0, + status=StatusChoices.FAIL, + passed_findings=0, + failed_findings=10, total_findings=10, ), ] @@ -884,12 +938,16 @@ class TestENSDimensionHandling: basic_ens_compliance_data ) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_ens_charts["create_radar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ens_report_module.DIMENSION_NAMES + assert chart_kwargs["values"] == [0.0, 0.0, 100, 100, 100] def test_dimensions_empty( self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute_opcional, + patched_ens_charts, ): """Test handling empty dimensions.""" # mock_ens_requirement_attribute_opcional has empty Dimensiones @@ -916,6 +974,9 @@ class TestENSDimensionHandling: basic_ens_compliance_data ) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_ens_charts["create_radar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ens_report_module.DIMENSION_NAMES + assert chart_kwargs["values"] == [100, 100, 100, 100, 100] # ============================================================================= @@ -1061,7 +1122,11 @@ class TestENSMarcoCategoryChart: """Test suite for ENS Marco/Categoría chart.""" def test_marco_category_chart_creation( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test that Marco/Categoría chart is created successfully.""" basic_ens_compliance_data.requirements = [ @@ -1086,9 +1151,17 @@ class TestENSMarcoCategoryChart: assert isinstance(chart_buffer, io.BytesIO) assert chart_buffer.getvalue() # Not empty + chart_kwargs = patched_ens_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Operacional - Gestión de incidentes"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["xlabel"] == "Porcentaje de Cumplimiento (%)" def test_marco_category_chart_excludes_manual( - self, ens_generator, basic_ens_compliance_data, mock_ens_requirement_attribute + self, + ens_generator, + basic_ens_compliance_data, + mock_ens_requirement_attribute, + patched_ens_charts, ): """Test that manual requirements are excluded from chart.""" basic_ens_compliance_data.requirements = [ @@ -1123,6 +1196,9 @@ class TestENSMarcoCategoryChart: basic_ens_compliance_data ) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_ens_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["Operacional - Gestión de incidentes"] + assert chart_kwargs["values"] == [100.0] # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_reports_nis2.py b/api/src/backend/tasks/tests/test_reports_nis2.py index 07e88ec7ca..ea6e06477d 100644 --- a/api/src/backend/tasks/tests/test_reports_nis2.py +++ b/api/src/backend/tasks/tests/test_reports_nis2.py @@ -2,9 +2,11 @@ import io from unittest.mock import Mock, patch import pytest -from reportlab.platypus import PageBreak, Paragraph, Table +from reportlab.platypus import Image, PageBreak, Paragraph, Table from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData +from tasks.jobs.reports import nis2 as nis2_report_module from tasks.jobs.reports.nis2 import NIS2ReportGenerator, _extract_section_number +from tasks.tests.report_test_helpers import patch_chart_helpers # Use string status values directly to avoid Django DB initialization @@ -29,6 +31,13 @@ def nis2_generator(): return NIS2ReportGenerator(config) +@pytest.fixture +def patched_nis2_charts(monkeypatch): + return patch_chart_helpers( + monkeypatch, nis2_report_module, ("create_horizontal_bar_chart",) + ) + + @pytest.fixture def mock_nis2_requirement_attribute_section1(): """Create a mock NIS2 requirement attribute for Section 1.""" @@ -380,7 +389,7 @@ class TestNIS2ChartsSection: """Test suite for NIS2 charts section generation.""" def test_charts_section_has_section_chart_title( - self, nis2_generator, basic_nis2_compliance_data + self, nis2_generator, basic_nis2_compliance_data, patched_nis2_charts ): """Test that charts section has section compliance title.""" basic_nis2_compliance_data.requirements = [] @@ -391,9 +400,14 @@ class TestNIS2ChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "Section" in content or "Compliance" in content + assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == [] + assert chart_kwargs["values"] == [] + assert chart_kwargs["xlabel"] == "Compliance (%)" def test_charts_section_has_page_break( - self, nis2_generator, basic_nis2_compliance_data + self, nis2_generator, basic_nis2_compliance_data, patched_nis2_charts ): """Test that charts section has page breaks.""" basic_nis2_compliance_data.requirements = [] @@ -403,12 +417,14 @@ class TestNIS2ChartsSection: page_breaks = [e for e in elements if isinstance(e, PageBreak)] assert len(page_breaks) >= 1 + assert len(patched_nis2_charts["create_horizontal_bar_chart"]) == 1 def test_charts_section_has_subsection_breakdown( self, nis2_generator, basic_nis2_compliance_data, mock_nis2_requirement_attribute_section1, + patched_nis2_charts, ): """Test that charts section includes subsection breakdown table.""" basic_nis2_compliance_data.requirements = [ @@ -434,6 +450,11 @@ class TestNIS2ChartsSection: paragraphs = [e for e in elements if isinstance(e, Paragraph)] content = " ".join(str(p.text) for p in paragraphs) assert "SubSection" in content or "Breakdown" in content + assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["1. Policy on Security"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["color_func"] # ============================================================================= @@ -449,6 +470,7 @@ class TestNIS2SectionChart: nis2_generator, basic_nis2_compliance_data, mock_nis2_requirement_attribute_section1, + patched_nis2_charts, ): """Test that section chart is created successfully.""" basic_nis2_compliance_data.requirements = [ @@ -473,12 +495,17 @@ class TestNIS2SectionChart: assert isinstance(chart_buffer, io.BytesIO) assert chart_buffer.getvalue() # Not empty + chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["1. Policy on Security"] + assert chart_kwargs["values"] == [100.0] + assert chart_kwargs["xlabel"] == "Compliance (%)" def test_section_chart_excludes_manual( self, nis2_generator, basic_nis2_compliance_data, mock_nis2_requirement_attribute_section1, + patched_nis2_charts, ): """Test that manual requirements are excluded from section chart.""" basic_nis2_compliance_data.requirements = [ @@ -515,6 +542,9 @@ class TestNIS2SectionChart: # Should not raise any errors chart_buffer = nis2_generator._create_section_chart(basic_nis2_compliance_data) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == ["1. Policy on Security"] + assert chart_kwargs["values"] == [100.0] def test_section_chart_multiple_sections( self, @@ -523,6 +553,7 @@ class TestNIS2SectionChart: mock_nis2_requirement_attribute_section1, mock_nis2_requirement_attribute_section2, mock_nis2_requirement_attribute_section11, + patched_nis2_charts, ): """Test section chart with multiple sections.""" basic_nis2_compliance_data.requirements = [ @@ -571,6 +602,13 @@ class TestNIS2SectionChart: chart_buffer = nis2_generator._create_section_chart(basic_nis2_compliance_data) assert isinstance(chart_buffer, io.BytesIO) + chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"] + assert chart_kwargs["labels"] == [ + "1. Policy on Security", + "2. Risk Management", + "11. Access Control", + ] + assert chart_kwargs["values"] == [100.0, 0.0, 100.0] # ============================================================================= diff --git a/api/src/backend/tasks/tests/test_reports_threatscore.py b/api/src/backend/tasks/tests/test_reports_threatscore.py index 07dd654a05..570a437eb3 100644 --- a/api/src/backend/tasks/tests/test_reports_threatscore.py +++ b/api/src/backend/tasks/tests/test_reports_threatscore.py @@ -10,6 +10,8 @@ from tasks.jobs.reports import ( RequirementData, ThreatScoreReportGenerator, ) +from tasks.jobs.reports import threatscore as threatscore_report_module +from tasks.tests.report_test_helpers import patch_chart_helpers # ============================================================================= # Fixtures @@ -23,6 +25,13 @@ def threatscore_generator(): return ThreatScoreReportGenerator(config) +@pytest.fixture +def patched_threatscore_charts(monkeypatch): + return patch_chart_helpers( + monkeypatch, threatscore_report_module, ("create_vertical_bar_chart",) + ) + + @pytest.fixture def mock_requirement_attribute(): """Create a mock requirement attribute with numeric values.""" @@ -677,7 +686,7 @@ class TestSectionScoreChart: """Test suite for section score chart generation.""" def test_create_section_chart_empty_data( - self, threatscore_generator, basic_compliance_data + self, threatscore_generator, basic_compliance_data, patched_threatscore_charts ): """Test chart creation with no requirements.""" basic_compliance_data.requirements = [] @@ -689,9 +698,22 @@ class TestSectionScoreChart: assert isinstance(result, io.BytesIO) assert result.getvalue() # Should have content + chart_kwargs = patched_threatscore_charts["create_vertical_bar_chart"][0][ + "kwargs" + ] + assert chart_kwargs["labels"] == [] + assert chart_kwargs["values"] == [] + assert chart_kwargs["ylabel"] == "Compliance Score (%)" + assert chart_kwargs["xlabel"] == "" + assert chart_kwargs["color_func"] + assert chart_kwargs["rotation"] == 0 def test_create_section_chart_single_section( - self, threatscore_generator, basic_compliance_data, mock_requirement_attribute + self, + threatscore_generator, + basic_compliance_data, + mock_requirement_attribute, + patched_threatscore_charts, ): """Test chart creation with a single section.""" basic_compliance_data.requirements = [ @@ -713,9 +735,14 @@ class TestSectionScoreChart: ) assert isinstance(result, io.BytesIO) + chart_kwargs = patched_threatscore_charts["create_vertical_bar_chart"][0][ + "kwargs" + ] + assert chart_kwargs["labels"] == ["1. IAM"] + assert chart_kwargs["values"] == [100.0] def test_create_section_chart_multiple_sections( - self, threatscore_generator, basic_compliance_data + self, threatscore_generator, basic_compliance_data, patched_threatscore_charts ): """Test chart creation with multiple sections.""" mock_attr_1 = Mock() @@ -756,9 +783,14 @@ class TestSectionScoreChart: ) assert isinstance(result, io.BytesIO) + chart_kwargs = patched_threatscore_charts["create_vertical_bar_chart"][0][ + "kwargs" + ] + assert chart_kwargs["labels"] == ["1. IAM", "2. Attack Surface"] + assert chart_kwargs["values"] == [100.0, 50.0] def test_create_section_chart_no_findings_section_gets_100( - self, threatscore_generator, basic_compliance_data + self, threatscore_generator, basic_compliance_data, patched_threatscore_charts ): """Test that sections without findings get 100% score.""" mock_attr = Mock() @@ -786,6 +818,11 @@ class TestSectionScoreChart: ) assert isinstance(result, io.BytesIO) + chart_kwargs = patched_threatscore_charts["create_vertical_bar_chart"][0][ + "kwargs" + ] + assert chart_kwargs["labels"] == ["1. IAM"] + assert chart_kwargs["values"] == [100.0] # ============================================================================= @@ -797,7 +834,11 @@ class TestExecutiveSummary: """Test suite for executive summary generation.""" def test_executive_summary_contains_chart( - self, threatscore_generator, basic_compliance_data, mock_requirement_attribute + self, + threatscore_generator, + basic_compliance_data, + mock_requirement_attribute, + patched_threatscore_charts, ): """Test that executive summary contains a chart.""" basic_compliance_data.requirements = [ @@ -818,9 +859,18 @@ class TestExecutiveSummary: assert len(elements) > 0 assert any(isinstance(e, Image) for e in elements) + chart_kwargs = patched_threatscore_charts["create_vertical_bar_chart"][0][ + "kwargs" + ] + assert chart_kwargs["labels"] == ["1. IAM"] + assert chart_kwargs["values"] == [100.0] def test_executive_summary_contains_score_table( - self, threatscore_generator, basic_compliance_data, mock_requirement_attribute + self, + threatscore_generator, + basic_compliance_data, + mock_requirement_attribute, + patched_threatscore_charts, ): """Test that executive summary contains a score table.""" basic_compliance_data.requirements = [ diff --git a/api/src/backend/tasks/tests/test_scan.py b/api/src/backend/tasks/tests/test_scan.py index 2fd2dde05f..f49ca6655b 100644 --- a/api/src/backend/tasks/tests/test_scan.py +++ b/api/src/backend/tasks/tests/test_scan.py @@ -76,7 +76,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): with ( patch("api.db_utils.rls_transaction"), @@ -134,7 +134,7 @@ class TestPerformScan: tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider # Ensure the provider type is 'aws' to match our mocks provider.provider = Provider.ProviderChoices.AWS @@ -243,11 +243,11 @@ class TestPerformScan: mock_prowler_scan_class, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider tenant_id = str(tenant.id) scan_id = str(scan.id) @@ -268,11 +268,11 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider tenant_id = str(tenant.id) scan_id = str(scan.id) @@ -304,11 +304,11 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider tenant_id = str(tenant.id) scan_id = str(scan.id) @@ -511,7 +511,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test that failed findings increment the failed_findings_count""" with ( @@ -532,7 +532,7 @@ class TestPerformScan: tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider # Ensure the provider type is 'aws' provider.provider = Provider.ProviderChoices.AWS @@ -589,7 +589,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test that multiple FAIL findings on the same resource increment the counter correctly""" with ( @@ -606,7 +606,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -706,7 +706,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test that muted FAIL findings do not increment the failed_findings_count""" with ( @@ -723,7 +723,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -777,13 +777,13 @@ class TestPerformScan: def test_perform_prowler_scan_reset_failed_findings_count( self, tenants_fixture, - providers_fixture, + aws_provider, resources_fixture, ): """Test that failed_findings_count is reset to 0 at the beginning of each scan""" # Use existing resource from fixture and set initial failed_findings_count tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider resource = resources_fixture[0] # Set a non-zero failed_findings_count initially @@ -956,7 +956,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test active MuteRule mutes findings with correct reason""" with ( @@ -973,7 +973,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -1073,7 +1073,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test inactive MuteRule does not mute findings""" with ( @@ -1090,7 +1090,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -1159,7 +1159,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test mutelist processor takes precedence over MuteRule""" with ( @@ -1176,7 +1176,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -1245,7 +1245,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test MuteRule with multiple finding UIDs mutes all findings""" with ( @@ -1262,7 +1262,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -1344,7 +1344,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test scan continues when MuteRule loading fails""" with ( @@ -1362,7 +1362,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -1427,7 +1427,7 @@ class TestPerformScan: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, ): """Test muted_at timestamp is set correctly for muted findings""" with ( @@ -1444,7 +1444,7 @@ class TestPerformScan: ): tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.AWS provider.save() @@ -2031,7 +2031,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, resources_fixture, ): @@ -2082,7 +2082,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch( @@ -2120,7 +2120,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): """Re-running compliance materialization must not raise nor duplicate rows. @@ -2175,7 +2175,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch( @@ -2183,7 +2183,7 @@ class TestCreateComplianceRequirements: ) as mock_compliance_template: tenant = tenants_fixture[0] scan = scans_fixture[0] - provider = providers_fixture[0] + provider = aws_provider provider.provider = Provider.ProviderChoices.KUBERNETES provider.save() @@ -2221,7 +2221,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch( @@ -2240,7 +2240,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch("tasks.jobs.scan.return_prowler_provider") as mock_prowler_provider: @@ -2324,7 +2324,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch( @@ -2362,7 +2362,7 @@ class TestCreateComplianceRequirements: self, tenants_fixture, scans_fixture, - providers_fixture, + aws_provider, findings_fixture, ): with patch( @@ -4689,7 +4689,7 @@ class TestUpdateProviderComplianceScores: self, mock_psycopg_connection, tenants_fixture, - providers_fixture, + aws_provider, scans_fixture, settings, ): @@ -4797,7 +4797,7 @@ class TestResetEphemeralResourceFindingsCount: ) def test_resets_only_resources_missing_from_full_scope_scan( - self, tenants_fixture, scans_fixture, providers_fixture, resources_fixture + self, tenants_fixture, scans_fixture, aws_provider, resources_fixture ): tenant, *_ = tenants_fixture scan1, scan2, *_ = scans_fixture @@ -4877,7 +4877,7 @@ class TestResetEphemeralResourceFindingsCount: assert result["reason"] == "scan not found" def test_skips_when_newer_scan_completed_for_same_provider( - self, tenants_fixture, scans_fixture, providers_fixture, resources_fixture + self, tenants_fixture, scans_fixture, aws_provider, resources_fixture ): # If a newer completed scan exists for the same provider, our # ResourceScanSummary set is stale relative to the resources' current @@ -4886,7 +4886,7 @@ class TestResetEphemeralResourceFindingsCount: tenant, *_ = tenants_fixture scan1, *_ = scans_fixture - provider, *_ = providers_fixture + provider = aws_provider _, resource2, _ = resources_fixture Resource.objects.filter(id=resource2.id).update(failed_findings_count=5) @@ -4916,7 +4916,7 @@ class TestResetEphemeralResourceFindingsCount: assert resource2.failed_findings_count == 5 def test_does_not_touch_other_providers_resources( - self, tenants_fixture, scans_fixture, providers_fixture, resources_fixture + self, tenants_fixture, scans_fixture, aws_provider, resources_fixture ): tenant, *_ = tenants_fixture scan1, *_ = scans_fixture @@ -4982,14 +4982,14 @@ class TestResetEphemeralResourceFindingsCount: assert resource2.failed_findings_count == 5 def test_ignores_sibling_scan_with_null_completed_at( - self, tenants_fixture, scans_fixture, providers_fixture, resources_fixture + self, tenants_fixture, scans_fixture, aws_provider, resources_fixture ): # Postgres orders NULL first in DESC; a sibling COMPLETED scan with a # missing completed_at must not be treated as the latest scan and # cause us to incorrectly skip the reset. tenant, *_ = tenants_fixture scan1, *_ = scans_fixture - provider, *_ = providers_fixture + provider = aws_provider resource1, resource2, _ = resources_fixture Resource.objects.filter(id=resource2.id).update(failed_findings_count=5) diff --git a/api/src/backend/tasks/tests/test_tasks.py b/api/src/backend/tasks/tests/test_tasks.py index 6475e525d6..631b91bf6b 100644 --- a/api/src/backend/tasks/tests/test_tasks.py +++ b/api/src/backend/tasks/tests/test_tasks.py @@ -1964,11 +1964,11 @@ class TestCleanupOrphanScheduledScans: ) def test_cleanup_deletes_orphan_when_both_available_and_scheduled_exist( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that AVAILABLE scan is deleted when SCHEDULED also exists.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Create orphan AVAILABLE scan @@ -2004,11 +2004,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=scheduled_scan.id).exists() def test_cleanup_does_not_delete_when_only_available_exists( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that AVAILABLE scan is NOT deleted when no SCHEDULED exists.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Create only AVAILABLE scan (normal first scan scenario) @@ -2033,11 +2033,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=available_scan.id).exists() def test_cleanup_does_not_delete_when_only_scheduled_exists( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that nothing is deleted when only SCHEDULED exists.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Create only SCHEDULED scan (normal subsequent scan scenario) @@ -2062,11 +2062,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=scheduled_scan.id).exists() def test_cleanup_returns_zero_when_no_scans_exist( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that cleanup returns 0 when no scans exist.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Execute cleanup with no scans @@ -2079,11 +2079,11 @@ class TestCleanupOrphanScheduledScans: assert deleted_count == 0 def test_cleanup_deletes_multiple_orphan_available_scans( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that multiple AVAILABLE orphan scans are all deleted.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Create multiple orphan AVAILABLE scans @@ -2128,12 +2128,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=scheduled_scan.id).exists() def test_cleanup_does_not_affect_different_provider( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider_pair ): """Test that cleanup only affects scans for the specified provider.""" tenant = tenants_fixture[0] - provider1 = providers_fixture[0] - provider2 = providers_fixture[1] + provider1, provider2 = aws_provider_pair periodic_task1 = self._create_periodic_task(provider1.id, tenant.id) periodic_task2 = self._create_periodic_task(provider2.id, tenant.id) @@ -2178,12 +2177,10 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=scheduled_scan_p1.id).exists() assert Scan.objects.filter(id=available_scan_p2.id).exists() - def test_cleanup_does_not_affect_manual_scans( - self, tenants_fixture, providers_fixture - ): + def test_cleanup_does_not_affect_manual_scans(self, tenants_fixture, aws_provider): """Test that cleanup only affects SCHEDULED trigger scans, not MANUAL.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) # Create orphan AVAILABLE scheduled scan @@ -2229,11 +2226,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=manual_scan.id).exists() def test_cleanup_does_not_affect_different_scheduler_task( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Test that cleanup only affects scans with the specified scheduler_task_id.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task1 = self._create_periodic_task(provider.id, tenant.id) # Create another periodic task @@ -2288,11 +2285,11 @@ class TestCleanupOrphanScheduledScans: assert Scan.objects.filter(id=available_scan_other_task.id).exists() def test_cleanup_keeps_db_queued_scheduled_scans( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """DB-queued scheduled scans have a task and must not be deleted as orphans.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) task_result = TaskResult.objects.create( task_id=str(uuid.uuid4()), @@ -2381,11 +2378,11 @@ class TestPerformScheduledScanTask: return task_result def test_queues_scheduled_scan_when_scheduled_scan_is_executing( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Queue a scheduled run when another scheduled scan is executing.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2431,11 +2428,11 @@ class TestPerformScheduledScanTask: ) def test_queues_scheduled_scan_when_manual_scan_is_pending( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Queue one scheduled run when a manual scan is already dispatched.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2488,11 +2485,11 @@ class TestPerformScheduledScanTask: assert scheduled_scan.scheduled_at > datetime.now(UTC) def test_coalesces_scheduled_scan_when_one_is_already_queued( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Reuse the existing queued scheduled scan instead of adding another.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2557,11 +2554,11 @@ class TestPerformScheduledScanTask: ) def test_creates_next_scheduled_scan_after_completion( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Create a next scheduled scan after a successful run completes.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2616,11 +2613,11 @@ class TestPerformScheduledScanTask: ) def test_next_scheduled_scan_failure_does_not_mask_completed_scan( - self, tenants_fixture, providers_fixture, caplog + self, tenants_fixture, aws_provider, caplog ): """Keep scheduled scan success when next-run creation fails.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2651,11 +2648,11 @@ class TestPerformScheduledScanTask: assert "Failed to ensure next scheduled scan" in caplog.text def test_dedupes_multiple_scheduled_scans_before_run( - self, tenants_fixture, providers_fixture + self, tenants_fixture, aws_provider ): """Ensure duplicated scheduled scans are removed before executing.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider periodic_task = self._create_periodic_task(provider.id, tenant.id) task_id = str(uuid.uuid4()) self._create_task_result(tenant.id, task_id) @@ -2767,12 +2764,12 @@ class TestPerformScanTask: def test_dispatches_next_queued_scan_after_completion( self, tenants_fixture, - providers_fixture, + aws_provider, django_capture_on_commit_callbacks, ): """Dispatch the next queued scan for the provider after completion.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider current_scan = Scan.objects.create( tenant_id=tenant.id, provider=provider, @@ -2830,11 +2827,11 @@ class TestPerformScanTask: ) def test_dispatch_failure_does_not_mask_completed_scan( - self, tenants_fixture, providers_fixture, caplog + self, tenants_fixture, aws_provider, caplog ): """Keep scan success when queued dispatch fails after completion.""" tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider current_scan = Scan.objects.create( tenant_id=tenant.id, provider=provider, diff --git a/docs/developer-guide/provider.mdx b/docs/developer-guide/provider.mdx index 9ad09b9f2a..7dbdeac01d 100644 --- a/docs/developer-guide/provider.mdx +++ b/docs/developer-guide/provider.mdx @@ -3212,21 +3212,18 @@ class YourProviderAPITestCase(APITestCase): #### 2.6.1. Add your mocked provider to the tests -If needed, add your mocked provider to the tests config file so you can use it on the tests. +If needed, add a named provider fixture or extend the provider factory defaults so tests can request only the provider they need. **File:** `api/src/backend/conftest.py` ```python @pytest.fixture -def providers_fixture(tenants_fixture): - tenant, *_ = tenants_fixture - providerX = Provider.objects.create( +def your_provider(provider_factory): + return provider_factory( provider="your_provider", uid="your_uid", alias="your_alias", - tenant_id=tenant.id, ) - return provider1, provider2, provider3, ... providerX ``` ### 2.7. Compliance and Output Support diff --git a/skills/prowler-test-api/SKILL.md b/skills/prowler-test-api/SKILL.md index 5b3e2ae6a1..2b5231104a 100644 --- a/skills/prowler-test-api/SKILL.md +++ b/skills/prowler-test-api/SKILL.md @@ -31,7 +31,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task ```text create_test_user (session) ─► tenants_fixture (function) ─► authenticated_client │ - └─► providers_fixture ─► scans_fixture ─► findings_fixture + └─► aws_provider ─► scans_fixture ─► findings_fixture ``` ### Key Fixtures @@ -40,8 +40,12 @@ create_test_user (session) ─► tenants_fixture (function) ─► authenticate |---------|-------------| | `create_test_user` | Session user (`dev@prowler.com`) | | `tenants_fixture` | 3 tenants: [0],[1] have membership, [2] isolated | -| `authenticated_client` | JWT client for tenant[0] | -| `providers_fixture` | 9 providers in tenant[0] | +| `authenticated_client` | Django test client with JWT for tenant[0] | +| `authenticated_client_for_tenant_factory` | Creates a Django test client with JWT for a specific user and tenant | +| `provider_factory` | Creates one validated provider with provider-specific defaults | +| `aws_provider` | 1 AWS provider in tenant[0] | +| `aws_provider_pair` | 2 AWS providers in tenant[0] | +| `all_provider_types_fixture` | 1 provider for every supported provider type | | `tasks_fixture` | 2 Celery tasks with TaskResult | ### RBAC Fixtures @@ -52,6 +56,14 @@ create_test_user (session) ─► tenants_fixture (function) ─► authenticate | `authenticated_client_rbac_noroles` | Membership but NO roles | | `authenticated_client_no_permissions_rbac` | All permissions = False | +Use `authenticated_client` for normal view behavior tests. It uses a cheap JWT +and still runs the real request authentication path. Use serializer-generated +JWTs or API-key clients only when the test is specifically about token +obtain/refresh, invalid tokens, expired tokens, tenant switching by token, API +keys, or unauthenticated 401 behavior. Use +`authenticated_client_for_tenant_factory` when a test needs a cheap JWT client +for a different user or tenant. + --- ## 2. JSON:API Requests diff --git a/skills/prowler-test-api/assets/api_test.py b/skills/prowler-test-api/assets/api_test.py index 0b70cd599d..e0ca148662 100644 --- a/skills/prowler-test-api/assets/api_test.py +++ b/skills/prowler-test-api/assets/api_test.py @@ -22,12 +22,12 @@ from api.rls import Tenant class TestProviderViewSet: """Example API tests for Provider endpoints.""" - def test_list_providers(self, authenticated_client, providers_fixture): + def test_list_providers(self, authenticated_client, aws_provider): """GET list returns all providers for authenticated tenant.""" response = authenticated_client.get(reverse("provider-list")) assert response.status_code == status.HTTP_200_OK - assert len(response.json()["data"]) == len(providers_fixture) + assert len(response.json()["data"]) == 1 def test_create_provider(self, authenticated_client): """POST with JSON:API format creates provider.""" @@ -49,9 +49,9 @@ class TestProviderViewSet: assert response.status_code == status.HTTP_201_CREATED assert response.json()["data"]["attributes"]["uid"] == "123456789012" - def test_update_provider(self, authenticated_client, providers_fixture): + def test_update_provider(self, authenticated_client, aws_provider): """PATCH with JSON:API format updates provider.""" - provider = providers_fixture[0] + provider = aws_provider payload = { "data": { @@ -95,7 +95,7 @@ class TestRLSIsolation: assert response.status_code == status.HTTP_404_NOT_FOUND def test_list_excludes_other_tenants( - self, authenticated_client, providers_fixture, tenants_fixture + self, authenticated_client, aws_provider, tenants_fixture ): """List endpoints only return resources from user's tenants.""" # Create provider in isolated tenant @@ -109,8 +109,8 @@ class TestRLSIsolation: response = authenticated_client.get(reverse("provider-list")) assert response.status_code == status.HTTP_200_OK - # Should only see providers_fixture (9 providers in tenant[0]) - assert len(response.json()["data"]) == len(providers_fixture) + # Should only see the AWS provider in tenant[0] + assert len(response.json()["data"]) == 1 @pytest.mark.django_db @@ -136,7 +136,7 @@ class TestRBACPermissions: response = authenticated_client_rbac_noroles.get(reverse("user-list")) assert response.status_code == status.HTTP_403_FORBIDDEN - def test_admin_sees_all(self, authenticated_client_rbac, providers_fixture): + def test_admin_sees_all(self, authenticated_client_rbac, aws_provider): """Admin with unlimited_visibility=True sees all providers.""" response = authenticated_client_rbac.get(reverse("provider-list")) assert response.status_code == status.HTTP_200_OK @@ -153,11 +153,11 @@ class TestAsyncOperations: mock_delete_task, mock_task_get, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): """DELETE returns 202 Accepted with Content-Location header.""" - provider = providers_fixture[0] + provider = aws_provider prowler_task = tasks_fixture[0] # Mock the Celery task @@ -184,11 +184,11 @@ class TestAsyncOperations: mock_scan_task, mock_task_get, authenticated_client, - providers_fixture, + aws_provider, tasks_fixture, ): """POST to scan trigger returns 202 with task location.""" - provider = providers_fixture[0] + provider = aws_provider prowler_task = tasks_fixture[0] task_mock = Mock() @@ -208,9 +208,9 @@ class TestAsyncOperations: class TestJSONAPIResponses: """Example JSON:API response handling.""" - def test_read_single_resource(self, authenticated_client, providers_fixture): + def test_read_single_resource(self, authenticated_client, aws_provider): """Read data from single resource response.""" - provider = providers_fixture[0] + provider = aws_provider response = authenticated_client.get( reverse("provider-detail", kwargs={"pk": provider.id}) ) @@ -222,12 +222,12 @@ class TestJSONAPIResponses: assert resource_id == str(provider.id) assert attrs["provider"] == provider.provider - def test_read_list_response(self, authenticated_client, providers_fixture): + def test_read_list_response(self, authenticated_client, aws_provider): """Read data from list response.""" response = authenticated_client.get(reverse("provider-list")) items = response.json()["data"] - assert len(items) == len(providers_fixture) + assert len(items) == 1 def test_read_relationships(self, authenticated_client, scans_fixture): """Read relationship data.""" @@ -262,9 +262,9 @@ class TestJSONAPIResponses: class TestSoftDelete: """Example soft-delete manager tests.""" - def test_objects_excludes_soft_deleted(self, providers_fixture): + def test_objects_excludes_soft_deleted(self, aws_provider): """Default manager excludes soft-deleted records.""" - provider = providers_fixture[0] + provider = aws_provider provider.is_deleted = True provider.save() @@ -284,12 +284,12 @@ class TestSoftDelete: class TestCeleryTaskLogic: """Example: Testing Celery task logic directly with apply().""" - def test_task_logic_directly(self, tenants_fixture, providers_fixture): + def test_task_logic_directly(self, tenants_fixture, aws_provider): """Use apply() for synchronous execution without Celery worker.""" from tasks.tasks import check_provider_connection_task tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider # Execute task synchronously (no broker needed) result = check_provider_connection_task.apply( @@ -328,12 +328,12 @@ class TestSetTenantDecorator: """Example: Testing @set_tenant decorator behavior.""" @patch("api.decorators.connection") - def test_sets_rls_context(self, mock_conn, tenants_fixture, providers_fixture): + def test_sets_rls_context(self, mock_conn, tenants_fixture, aws_provider): """Verify @set_tenant sets RLS context via SET_CONFIG_QUERY.""" from tasks.tasks import check_provider_connection_task tenant = tenants_fixture[0] - provider = providers_fixture[0] + provider = aws_provider # Call task with tenant_id - decorator sets RLS and pops it check_provider_connection_task.apply( @@ -349,13 +349,13 @@ class TestBeatScheduling: """Example: Testing Beat scheduled task creation.""" @patch("tasks.beat.perform_scheduled_scan_task.apply_async") - def test_schedule_provider_scan(self, mock_apply, providers_fixture): + def test_schedule_provider_scan(self, mock_apply, aws_provider): """Verify periodic task is created with correct settings.""" from django_celery_beat.models import PeriodicTask from tasks.beat import schedule_provider_scan - provider = providers_fixture[0] + provider = aws_provider mock_apply.return_value = Mock(id="task-123") schedule_provider_scan(provider) diff --git a/skills/prowler-test-api/references/test-api-docs.md b/skills/prowler-test-api/references/test-api-docs.md index 0150fbfe87..02d450e788 100644 --- a/skills/prowler-test-api/references/test-api-docs.md +++ b/skills/prowler-test-api/references/test-api-docs.md @@ -24,7 +24,7 @@ create_test_user (session) │ └─► authenticated_client │ └─► (most API tests use this) │ - ├─► providers_fixture + ├─► aws_provider │ └─► scans_fixture │ └─► findings_fixture │ @@ -102,12 +102,20 @@ Authentication tests: ```python @pytest.mark.django_db class TestProviderViewSet: - def test_list(self, authenticated_client, providers_fixture): - # authenticated_client has JWT for tenant[0] - # providers_fixture has 9 providers in tenant[0] + def test_list(self, authenticated_client, aws_provider): + # authenticated_client is a Django test client with JWT for tenant[0] + # aws_provider creates one validated AWS provider in tenant[0] ... ``` +Use serializer-generated JWTs or API-key clients for authentication behavior +tests only: token obtain/refresh, invalid or expired tokens, token-scoped tenant +switching, API keys, and unauthenticated 401 responses. Regular view tests +should use `authenticated_client` so they still exercise `request.user`, +`request.auth["tenant_id"]`, RLS, and RBAC without paying token serializer cost. +Use `authenticated_client_for_tenant_factory` when a test needs the same cheap +JWT path for a different user or tenant. + ### RBAC Tests ```python diff --git a/skills/tdd/SKILL.md b/skills/tdd/SKILL.md index d62d359053..6b82228c9f 100644 --- a/skills/tdd/SKILL.md +++ b/skills/tdd/SKILL.md @@ -164,9 +164,9 @@ class Test_ec2_ami_public: ```python @pytest.mark.django_db class TestResourceModel: - def test_create_resource_with_tags(self, providers_fixture): + def test_create_resource_with_tags(self, aws_provider): # Given - provider, *_ = providers_fixture + provider = aws_provider tenant_id = provider.tenant_id # When