mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-29 23:04:23 +00:00
feat/PRWLR-4002 add /scans endpoints (without SDK integration) (#33)
* feat(Backend): PRWLR-4992 add Scan db model * feat(API): PRWLR-4002 add viewset and required tools for /scans endpoint * feat(API): PRWLR-4002 improve /scans filters * feat(API): PRWLR-4002 add relationships links * feat(API): PRWLR-4002 implement POST /scan custom logic * fix(API): PRWLR-4002 fix Scan.type keyword usage * feat(API): PRWLR-4002 implement PATCH /scans * feat(API): PRWLR-4002 refactor serializers for write operations * fix(API): PRWLR-4002 fix providers on ScanSerializer * test(API): PRWLR-4002 add unit tests for /scans * feat(Backend, Test): PRWLR-4002 refactor routing and unit tests db connections * build(CI): PRWLR-4002 update CI env vars for postgres * fix(Tests): PRWLR-4002 fix close_db_connections fixture * feat(Backend, API): PRWLR-4002 apply requested changes to Scan model and filters * chore(Tests): PRWLR-4002 rename pytest fixtures * feat(Backend): PRWLR-4002 remove unique constraint from Scan model * fix(Backend, Tests): PRWLR-4002 fix db routing and migration mechanism for tests * chore(deps): PRWLR-4002 add uuid6 to poetry deps * chore(Backend): PRWLR-4002 refactor filter methods * fix(Tests): PRWLR-4002 fix wrong postgres credentials for testing * feat(API): PRWLR-4002 implement merging mechanism for Scans.scanner_args * feat(API): PRWLR-4002 implement merging mechanism for Scans.scanner_args * feat(Backend): PRWLR-4002 add indexes to Scan model * feat(Backend): PRWLR-4002 remove id index from Scan model (redundant) * feat(API): PRWLR-4002 add datetime fields to Scan serializer
This commit is contained in:
1 parent
8183207802
commit
24857eaa7f
23 files changed
+1379
-409
No files matched your search
@@ -109,3 +109,27 @@ class ProviderEnum(EnumType):
|
||||
class ProviderEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("provider", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Scan.type
|
||||
|
||||
|
||||
class ScanTypeEnum(EnumType):
|
||||
enum_type_name = "scan_type"
|
||||
|
||||
|
||||
class ScanTypeEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("scan_type", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for state
|
||||
|
||||
|
||||
class StateEnum(EnumType):
|
||||
enum_type_name = "state"
|
||||
|
||||
|
||||
class StateEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("state", *args, **kwargs)
|
||||
@@ -1,11 +1,44 @@
|
||||
from django_filters.rest_framework import FilterSet, BooleanFilter, CharFilter
|
||||
from rest_framework_json_api.django_filters.backends import DjangoFilterBackend
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import ProviderEnumField
|
||||
from api.models import Provider
|
||||
from api.models import Provider, Scan
|
||||
from api.rls import Tenant
|
||||
|
||||
|
||||
def provider_enum_filter(queryset, value, lookup_field: str = "provider"):
|
||||
"""
|
||||
Filter a queryset based on a provider value, using a specified lookup field.
|
||||
|
||||
This function filters a given queryset by checking if the provided `value`
|
||||
matches a valid choice in the `Provider.ProviderChoices` enum. If the `value`
|
||||
is valid, the queryset is filtered using the specified `lookup_field`.
|
||||
Otherwise, a `ValidationError` is raised.
|
||||
|
||||
Args:
|
||||
queryset (QuerySet): The Django queryset to be filtered.
|
||||
value (str): The value to filter the queryset by, which must be a valid
|
||||
option in `Provider.ProviderChoices`.
|
||||
lookup_field (str): The field or lookup path within the model used for
|
||||
filtering the queryset. Defaults to "provider".
|
||||
|
||||
Returns:
|
||||
QuerySet: A filtered queryset based on the provided `value` and `lookup_field`.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the provided `value` is not a valid choice in
|
||||
`Provider.ProviderChoices`.
|
||||
"""
|
||||
if value not in Provider.ProviderChoices:
|
||||
raise ValidationError(
|
||||
f"Invalid provider value: '{value}'. Valid values are: "
|
||||
f"{', '.join(Provider.ProviderChoices)}"
|
||||
)
|
||||
|
||||
return queryset.filter(**{lookup_field: value})
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
def to_html(self, _request, _queryset, _view):
|
||||
"""Override this method to use the Browsable API in dev environments.
|
||||
@@ -27,6 +60,10 @@ class TenantFilter(FilterSet):
|
||||
|
||||
class ProviderFilter(FilterSet):
|
||||
connected = BooleanFilter()
|
||||
provider = CharFilter(method="filter_provider")
|
||||
|
||||
def filter_provider(self, queryset, name, value):
|
||||
return provider_enum_filter(queryset, value)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
@@ -42,3 +79,30 @@ class ProviderFilter(FilterSet):
|
||||
"filter_class": CharFilter,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ScanFilter(FilterSet):
|
||||
provider = CharFilter(method="filter_provider")
|
||||
type = CharFilter(method="filter_type")
|
||||
|
||||
def filter_provider(self, queryset, name, value):
|
||||
return provider_enum_filter(queryset, value, lookup_field="provider__provider")
|
||||
|
||||
def filter_type(self, queryset, name, value):
|
||||
if value not in Scan.TypeChoices:
|
||||
raise ValidationError(
|
||||
f"Invalid scan type value: '{value}'. Valid values are: "
|
||||
f"{', '.join(Scan.TypeChoices)}"
|
||||
)
|
||||
|
||||
return queryset.filter(type=value)
|
||||
|
||||
class Meta:
|
||||
model = Scan
|
||||
fields = {
|
||||
"provider": ["exact"],
|
||||
"provider_id": ["exact"],
|
||||
"name": ["exact", "icontains"],
|
||||
"started_at": ["exact", "gte", "lte"],
|
||||
"type": ["exact"],
|
||||
}
|
||||
@@ -3,27 +3,47 @@ from functools import partial
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from uuid6 import uuid7
|
||||
|
||||
import api.rls
|
||||
from api.db_utils import (
|
||||
PostgresEnumMigration,
|
||||
register_enum,
|
||||
ProviderEnumField,
|
||||
ProviderEnum,
|
||||
ProviderEnumField,
|
||||
ScanTypeEnum,
|
||||
StateEnumField,
|
||||
StateEnum,
|
||||
ScanTypeEnumField,
|
||||
register_enum,
|
||||
)
|
||||
from api.models import Provider
|
||||
from api.models import Provider, Scan, StateChoices
|
||||
|
||||
DB_NAME = settings.DATABASES["default"]["NAME"]
|
||||
DB_USER_NAME = settings.DATABASES["default"]["USER"]
|
||||
DB_USER_PASSWORD = settings.DATABASES["default"]["PASSWORD"]
|
||||
DB_USER_NAME = (
|
||||
settings.DATABASES["prowler_user"]["USER"] if not settings.TESTING else "test"
|
||||
)
|
||||
DB_USER_PASSWORD = (
|
||||
settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test"
|
||||
)
|
||||
|
||||
ProviderEnumMigration = PostgresEnumMigration(
|
||||
enum_name="provider",
|
||||
enum_values=tuple(provider[0] for provider in Provider.ProviderChoices.choices),
|
||||
)
|
||||
|
||||
ScanTypeEnumMigration = PostgresEnumMigration(
|
||||
enum_name="scan_type",
|
||||
enum_values=tuple(scan_type[0] for scan_type in Scan.TypeChoices.choices),
|
||||
)
|
||||
|
||||
StateEnumMigration = PostgresEnumMigration(
|
||||
enum_name="state",
|
||||
enum_values=tuple(state[0] for state in StateChoices.choices),
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
@@ -54,6 +74,12 @@ class Migration(migrations.Migration):
|
||||
GRANT SELECT ON django_migrations TO {DB_USER_NAME};
|
||||
"""
|
||||
),
|
||||
# Create and register State type
|
||||
migrations.RunPython(
|
||||
StateEnumMigration.create_enum_type,
|
||||
reverse_code=StateEnumMigration.drop_enum_type,
|
||||
),
|
||||
migrations.RunPython(partial(register_enum, enum_class=StateEnum)),
|
||||
migrations.CreateModel(
|
||||
name="Tenant",
|
||||
fields=[
|
||||
@@ -162,4 +188,99 @@ class Migration(migrations.Migration):
|
||||
name="unique_provider_ids",
|
||||
),
|
||||
),
|
||||
# Create and register ScanTypeEnum type
|
||||
migrations.RunPython(
|
||||
ScanTypeEnumMigration.create_enum_type,
|
||||
reverse_code=ScanTypeEnumMigration.drop_enum_type,
|
||||
),
|
||||
migrations.RunPython(partial(register_enum, enum_class=ScanTypeEnum)),
|
||||
migrations.CreateModel(
|
||||
name="Scan",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
default=uuid7,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=100,
|
||||
null=True,
|
||||
validators=[django.core.validators.MinLengthValidator(3)],
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
ScanTypeEnumField(
|
||||
choices=[("scheduled", "Scheduled"), ("manual", "Manual")]
|
||||
),
|
||||
),
|
||||
(
|
||||
"state",
|
||||
StateEnumField(
|
||||
choices=[
|
||||
("available", "Available"),
|
||||
("scheduled", "Scheduled"),
|
||||
("executing", "Executing"),
|
||||
("completed", "Completed"),
|
||||
("failed", "Failed"),
|
||||
("cancelled", "Cancelled"),
|
||||
],
|
||||
default="available",
|
||||
),
|
||||
),
|
||||
("unique_resource_count", models.IntegerField(default=0)),
|
||||
("progress", models.IntegerField(default=0)),
|
||||
("scanner_args", models.JSONField(default=dict)),
|
||||
("duration", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"scheduled_at",
|
||||
models.DateTimeField(null=True, blank=True),
|
||||
),
|
||||
("inserted_at", models.DateTimeField(auto_now_add=True)),
|
||||
("updated_at", models.DateTimeField(auto_now=True)),
|
||||
("started_at", models.DateTimeField(null=True, blank=True)),
|
||||
("completed_at", models.DateTimeField(null=True, blank=True)),
|
||||
(
|
||||
"provider",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="scans",
|
||||
related_query_name="scan",
|
||||
to="api.provider",
|
||||
),
|
||||
),
|
||||
(
|
||||
"tenant",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="api.tenant"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"db_table": "scans",
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="scan",
|
||||
constraint=api.rls.RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name="rls_on_scan",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="scan",
|
||||
index=models.Index(
|
||||
fields=["provider", "state", "type", "scheduled_at"],
|
||||
name="scans_prov_state_type_sche_idx",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -4,12 +4,23 @@ from uuid import uuid4, UUID
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from api.db_utils import ProviderEnumField
|
||||
from uuid6 import uuid7
|
||||
|
||||
from api.db_utils import ProviderEnumField, StateEnumField, ScanTypeEnumField
|
||||
from api.exceptions import ModelValidationError
|
||||
from api.rls import RowLevelSecurityConstraint
|
||||
from api.rls import RowLevelSecurityProtectedModel
|
||||
|
||||
|
||||
class StateChoices(models.TextChoices):
|
||||
AVAILABLE = "available", _("Available")
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
EXECUTING = "executing", _("Executing")
|
||||
COMPLETED = "completed", _("Completed")
|
||||
FAILED = "failed", _("Failed")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
|
||||
|
||||
class Provider(RowLevelSecurityProtectedModel):
|
||||
class ProviderChoices(models.TextChoices):
|
||||
AWS = "aws", _("AWS")
|
||||
@@ -96,3 +107,53 @@ class Provider(RowLevelSecurityProtectedModel):
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Scan(RowLevelSecurityProtectedModel):
|
||||
class TypeChoices(models.TextChoices):
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
MANUAL = "manual", _("Manual")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
|
||||
name = models.CharField(
|
||||
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scans",
|
||||
related_query_name="scan",
|
||||
)
|
||||
type = ScanTypeEnumField(
|
||||
choices=TypeChoices.choices,
|
||||
)
|
||||
state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE)
|
||||
unique_resource_count = models.IntegerField(default=0)
|
||||
progress = models.IntegerField(default=0)
|
||||
scanner_args = models.JSONField(default=dict)
|
||||
duration = models.IntegerField(null=True, blank=True)
|
||||
scheduled_at = models.DateTimeField(null=True, blank=True)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
# TODO: task foreign key
|
||||
# TODO: mutelist foreign key
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "scans"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["provider", "state", "type", "scheduled_at"],
|
||||
name="scans_prov_state_type_sche_idx",
|
||||
),
|
||||
]
|
||||
@@ -7,6 +7,10 @@ from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db import models
|
||||
from django.db.backends.ddl_references import Statement, Table
|
||||
|
||||
DB_PROWLER_USER = (
|
||||
settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
|
||||
)
|
||||
|
||||
|
||||
class Tenant(models.Model):
|
||||
"""
|
||||
@@ -84,7 +88,7 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
|
||||
full_create_sql_query,
|
||||
table_name=model._meta.db_table,
|
||||
field_column=field_column,
|
||||
db_user=settings.DATABASES["default"]["USER"],
|
||||
db_user=DB_PROWLER_USER,
|
||||
tenant_setting=self.TENANT_SETTING,
|
||||
)
|
||||
|
||||
@@ -98,7 +102,7 @@ class RowLevelSecurityConstraint(models.BaseConstraint):
|
||||
full_drop_sql_query,
|
||||
table_name=Table(model._meta.db_table, schema_editor.quote_name),
|
||||
field_column=field_column,
|
||||
db_user=settings.DATABASES["default"]["USER"],
|
||||
db_user=DB_PROWLER_USER,
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_check_resources_between_different_tenants(
|
||||
enforce_test_user_db_connection, tenants_fixture
|
||||
):
|
||||
client = APIClient()
|
||||
|
||||
tenant1 = str(tenants_fixture[0].id)
|
||||
tenant2 = str(tenants_fixture[1].id)
|
||||
|
||||
# Create a provider on tenant 1
|
||||
provider_data = {
|
||||
"data": {
|
||||
"type": "Provider",
|
||||
"attributes": {
|
||||
"alias": "test_provider_tenant_1",
|
||||
"provider": "aws",
|
||||
"provider_id": "123456789012",
|
||||
},
|
||||
}
|
||||
}
|
||||
provider1_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_data,
|
||||
format="vnd.api+json",
|
||||
HTTP_X_TENANT_ID=tenant1,
|
||||
)
|
||||
assert provider1_response.status_code == 201
|
||||
provider1_id = provider1_response.json()["data"]["id"]
|
||||
|
||||
# Create a provider on tenant 2
|
||||
provider_data = {
|
||||
"data": {
|
||||
"type": "Provider",
|
||||
"attributes": {
|
||||
"alias": "test_provider_tenant_2",
|
||||
"provider": "aws",
|
||||
"provider_id": "123456789013",
|
||||
},
|
||||
}
|
||||
}
|
||||
provider2_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_data,
|
||||
format="vnd.api+json",
|
||||
HTTP_X_TENANT_ID=tenant2,
|
||||
)
|
||||
assert provider2_response.status_code == 201
|
||||
provider2_id = provider2_response.json()["data"]["id"]
|
||||
|
||||
# Try to get the provider from tenant 1 on tenant 2 and vice versa
|
||||
tenant1_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1_id}),
|
||||
HTTP_X_TENANT_ID=tenant2,
|
||||
)
|
||||
assert tenant1_response.status_code == 404
|
||||
tenant2_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1_id}),
|
||||
HTTP_X_TENANT_ID=tenant1,
|
||||
)
|
||||
assert tenant2_response.status_code == 200
|
||||
assert tenant2_response.json()["data"]["id"] == provider1_id
|
||||
|
||||
# Vice versa
|
||||
|
||||
tenant2_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider2_id}),
|
||||
HTTP_X_TENANT_ID=tenant1,
|
||||
)
|
||||
assert tenant2_response.status_code == 404
|
||||
tenant1_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider2_id}),
|
||||
HTTP_X_TENANT_ID=tenant2,
|
||||
)
|
||||
assert tenant1_response.status_code == 200
|
||||
assert tenant1_response.json()["data"]["id"] == provider2_id
|
||||
@@ -18,14 +18,12 @@ class TestMainDatabaseRouter:
|
||||
|
||||
@pytest.mark.parametrize("api_model", [Tenant])
|
||||
def test_router_api_models(self, api_model, router):
|
||||
assert router.db_for_read(api_model) == MainRouter.default_db
|
||||
assert router.db_for_write(api_model) == MainRouter.default_db
|
||||
assert router.db_for_read(api_model) == "default"
|
||||
assert router.db_for_write(api_model) == "default"
|
||||
|
||||
assert router.allow_migrate_model(MainRouter.admin_db, api_model)
|
||||
assert not router.allow_migrate_model(MainRouter.default_db, api_model)
|
||||
assert not router.allow_migrate_model("default", api_model)
|
||||
|
||||
def test_router_django_models(self, router):
|
||||
assert router.db_for_read(MigrationRecorder.Migration) == MainRouter.admin_db
|
||||
assert (
|
||||
not router.db_for_read(MigrationRecorder.Migration) == MainRouter.default_db
|
||||
)
|
||||
assert not router.db_for_read(MigrationRecorder.Migration) == "default"
|
||||
@@ -0,0 +1,76 @@
|
||||
from api.utils import merge_dicts
|
||||
|
||||
|
||||
class TestMergeDicts:
|
||||
def test_simple_merge(self):
|
||||
default_dict = {"key1": "value1", "key2": "value2"}
|
||||
replacement_dict = {"key2": "new_value2", "key3": "value3"}
|
||||
expected_result = {"key1": "value1", "key2": "new_value2", "key3": "value3"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_nested_merge(self):
|
||||
default_dict = {
|
||||
"key1": "value1",
|
||||
"key2": {"nested_key1": "nested_value1", "nested_key2": "nested_value2"},
|
||||
}
|
||||
replacement_dict = {
|
||||
"key2": {
|
||||
"nested_key2": "new_nested_value2",
|
||||
"nested_key3": "nested_value3",
|
||||
},
|
||||
"key3": "value3",
|
||||
}
|
||||
expected_result = {
|
||||
"key1": "value1",
|
||||
"key2": {
|
||||
"nested_key1": "nested_value1",
|
||||
"nested_key2": "new_nested_value2",
|
||||
"nested_key3": "nested_value3",
|
||||
},
|
||||
"key3": "value3",
|
||||
}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_no_overlap(self):
|
||||
default_dict = {"key1": "value1"}
|
||||
replacement_dict = {"key2": "value2"}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_replacement_dict_empty(self):
|
||||
default_dict = {"key1": "value1", "key2": "value2"}
|
||||
replacement_dict = {}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_default_dict_empty(self):
|
||||
default_dict = {}
|
||||
replacement_dict = {"key1": "value1", "key2": "value2"}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_nested_empty_in_replacement_dict(self):
|
||||
default_dict = {"key1": {"nested_key1": "nested_value1"}}
|
||||
replacement_dict = {"key1": {}}
|
||||
expected_result = {"key1": {}}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_deep_nested_merge(self):
|
||||
default_dict = {"key1": {"nested_key1": {"deep_key1": "deep_value1"}}}
|
||||
replacement_dict = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}}
|
||||
expected_result = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
+304
-101
@@ -4,30 +4,13 @@ import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
|
||||
from api.models import Provider
|
||||
from api.models import Provider, Scan
|
||||
from api.rls import Tenant
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
# TODO Change to 401 when authentication/authorization is implemented
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN
|
||||
from conftest import API_JSON_CONTENT_TYPE, NO_TENANT_HTTP_STATUS
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestTenantViewSet:
|
||||
@pytest.fixture
|
||||
def tenants(self):
|
||||
tenant1 = Tenant.objects.create(
|
||||
name="Tenant One",
|
||||
inserted_at="2023-01-01T00:00:00Z",
|
||||
updated_at="2023-01-02T00:00:00Z",
|
||||
)
|
||||
tenant2 = Tenant.objects.create(
|
||||
name="Tenant Two",
|
||||
inserted_at="2023-01-03T00:00:00Z",
|
||||
updated_at="2023-01-04T00:00:00Z",
|
||||
)
|
||||
return tenant1, tenant2
|
||||
|
||||
@pytest.fixture
|
||||
def valid_tenant_payload(self):
|
||||
return {
|
||||
@@ -44,13 +27,13 @@ class TestTenantViewSet:
|
||||
"updated_at": "2023-01-06T00:00:00Z",
|
||||
}
|
||||
|
||||
def test_tenants_list(self, client, tenants):
|
||||
def test_tenants_list(self, client, tenants_fixture):
|
||||
response = client.get(reverse("tenant-list"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(tenants)
|
||||
assert len(response.json()["data"]) == len(tenants_fixture)
|
||||
|
||||
def test_tenants_retrieve(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_retrieve(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.get(reverse("tenant-detail", kwargs={"pk": tenant1.id}))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["name"] == tenant1.name
|
||||
@@ -76,8 +59,8 @@ class TestTenantViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenants_partial_update(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_partial_update(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
new_name = "This is the new name"
|
||||
payload = {
|
||||
"data": {
|
||||
@@ -95,15 +78,15 @@ class TestTenantViewSet:
|
||||
tenant1.refresh_from_db()
|
||||
assert tenant1.name == new_name
|
||||
|
||||
def test_tenants_partial_update_invalid_content_type(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_partial_update_invalid_content_type(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.patch(
|
||||
reverse("tenant-detail", kwargs={"pk": tenant1.id}), data={}
|
||||
)
|
||||
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
|
||||
def test_tenants_partial_update_invalid_content(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_partial_update_invalid_content(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
new_name = "This is the new name"
|
||||
payload = {"name": new_name}
|
||||
response = client.patch(
|
||||
@@ -113,8 +96,8 @@ class TestTenantViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenants_delete(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_delete(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.delete(reverse("tenant-detail", kwargs={"pk": tenant1.id}))
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert Tenant.objects.count() == 1
|
||||
@@ -125,16 +108,16 @@ class TestTenantViewSet:
|
||||
# (user might not have permissions to see if the tenant exists or not -> 200 empty)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_tenants_list_filter_search(self, client, tenants):
|
||||
"""Search is applied to tenants name."""
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_list_filter_search(self, client, tenants_fixture):
|
||||
"""Search is applied to tenants_fixture name."""
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.get(reverse("tenant-list"), {"filter[search]": tenant1.name})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
assert response.json()["data"][0]["attributes"]["name"] == tenant1.name
|
||||
|
||||
def test_tenants_list_query_param_name(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_list_query_param_name(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.get(reverse("tenant-list"), {"name": tenant1.name})
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@@ -142,8 +125,8 @@ class TestTenantViewSet:
|
||||
response = client.get(reverse("tenant-list"), {"random": "value"})
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenants_list_filter_name(self, client, tenants):
|
||||
tenant1, _ = tenants
|
||||
def test_tenants_list_filter_name(self, client, tenants_fixture):
|
||||
tenant1, _ = tenants_fixture
|
||||
response = client.get(reverse("tenant-list"), {"filter[name]": tenant1.name})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 1
|
||||
@@ -153,16 +136,16 @@ class TestTenantViewSet:
|
||||
response = client.get(reverse("tenant-list"), {"filter[invalid]": "whatever"})
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_tenants_list_page_size(self, client, tenants):
|
||||
def test_tenants_list_page_size(self, client, tenants_fixture):
|
||||
page_size = 1
|
||||
|
||||
response = client.get(reverse("tenant-list"), {"page[size]": page_size})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == page_size
|
||||
assert response.json()["meta"]["pagination"]["page"] == 1
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants)
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
|
||||
|
||||
def test_tenants_list_page_number(self, client, tenants):
|
||||
def test_tenants_list_page_number(self, client, tenants_fixture):
|
||||
page_size = 1
|
||||
page_number = 2
|
||||
|
||||
@@ -173,10 +156,10 @@ class TestTenantViewSet:
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == page_size
|
||||
assert response.json()["meta"]["pagination"]["page"] == page_number
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants)
|
||||
assert response.json()["meta"]["pagination"]["pages"] == len(tenants_fixture)
|
||||
|
||||
def test_tenants_list_sort_name(self, client, tenants):
|
||||
_, tenant2 = tenants
|
||||
def test_tenants_list_sort_name(self, client, tenants_fixture):
|
||||
_, tenant2 = tenants_fixture
|
||||
response = client.get(reverse("tenant-list"), {"sort": "-name"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == 2
|
||||
@@ -185,53 +168,17 @@ class TestTenantViewSet:
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestProviderViewSet:
|
||||
@pytest.fixture
|
||||
def providers(self, get_tenant):
|
||||
tenant = get_tenant
|
||||
provider1 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789012",
|
||||
alias="aws_testing_1",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider2 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789013",
|
||||
alias="aws_testing_2",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider3 = Provider.objects.create(
|
||||
provider="gcp",
|
||||
provider_id="a12322-test321",
|
||||
alias="gcp_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider4 = Provider.objects.create(
|
||||
provider="kubernetes",
|
||||
provider_id="kubernetes-test-12345",
|
||||
alias="k8s_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider5 = Provider.objects.create(
|
||||
provider="azure",
|
||||
provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
alias="azure_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
|
||||
return provider1, provider2, provider3, provider4, provider5
|
||||
|
||||
def test_providers_rls(self, client):
|
||||
response = client.get(reverse("provider-list"))
|
||||
assert response.status_code == NO_TENANT_HTTP_STATUS
|
||||
|
||||
def test_providers_list(self, client, providers, tenant_header):
|
||||
def test_providers_list(self, client, providers_fixture, tenant_header):
|
||||
response = client.get(reverse("provider-list"), headers=tenant_header)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(providers)
|
||||
assert len(response.json()["data"]) == len(providers_fixture)
|
||||
|
||||
def test_providers_retrieve(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
def test_providers_retrieve(self, client, providers_fixture, tenant_header):
|
||||
provider1, *_ = providers_fixture
|
||||
response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
headers=tenant_header,
|
||||
@@ -286,12 +233,13 @@ class TestProviderViewSet:
|
||||
assert Provider.objects.get().alias == provider_json_payload["alias"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"provider_json_payload, error_code",
|
||||
"provider_json_payload, error_code, error_pointer",
|
||||
(
|
||||
[
|
||||
(
|
||||
{"provider": "aws", "provider_id": "1", "alias": "test"},
|
||||
"min_length",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -300,14 +248,17 @@ class TestProviderViewSet:
|
||||
"alias": "test",
|
||||
},
|
||||
"aws-provider-id",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{"provider": "aws", "provider_id": "aaaaaaaaaaaa", "alias": "test"},
|
||||
"aws-provider-id",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{"provider": "gcp", "provider_id": "1234asdf", "alias": "test"},
|
||||
"gcp-provider-id",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -316,6 +267,7 @@ class TestProviderViewSet:
|
||||
"alias": "test",
|
||||
},
|
||||
"kubernetes-provider-id",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{
|
||||
@@ -324,12 +276,22 @@ class TestProviderViewSet:
|
||||
"alias": "test",
|
||||
},
|
||||
"azure-provider-id",
|
||||
"provider_id",
|
||||
),
|
||||
(
|
||||
{
|
||||
"provider": "does-not-exist",
|
||||
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e87",
|
||||
"alias": "test",
|
||||
},
|
||||
"invalid_choice",
|
||||
"provider",
|
||||
),
|
||||
]
|
||||
),
|
||||
)
|
||||
def test_providers_invalid_create(
|
||||
self, client, tenant_header, provider_json_payload, error_code
|
||||
self, client, tenant_header, provider_json_payload, error_code, error_pointer
|
||||
):
|
||||
response = client.post(
|
||||
reverse("provider-list"),
|
||||
@@ -341,11 +303,11 @@ class TestProviderViewSet:
|
||||
assert response.json()["errors"][0]["code"] == error_code
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"]
|
||||
== "/data/attributes/provider_id"
|
||||
== f"/data/attributes/{error_pointer}"
|
||||
)
|
||||
|
||||
def test_providers_partial_update(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
def test_providers_partial_update(self, client, providers_fixture, tenant_header):
|
||||
provider1, *_ = providers_fixture
|
||||
new_alias = "This is the new name"
|
||||
payload = {
|
||||
"data": {
|
||||
@@ -365,9 +327,9 @@ class TestProviderViewSet:
|
||||
assert provider1.alias == new_alias
|
||||
|
||||
def test_providers_partial_update_invalid_content_type(
|
||||
self, client, providers, tenant_header
|
||||
self, client, providers_fixture, tenant_header
|
||||
):
|
||||
provider1, *_ = providers
|
||||
provider1, *_ = providers_fixture
|
||||
response = client.patch(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
data={},
|
||||
@@ -376,9 +338,9 @@ class TestProviderViewSet:
|
||||
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
|
||||
def test_providers_partial_update_invalid_content(
|
||||
self, client, providers, tenant_header
|
||||
self, client, providers_fixture, tenant_header
|
||||
):
|
||||
provider1, *_ = providers
|
||||
provider1, *_ = providers_fixture
|
||||
new_name = "This is the new name"
|
||||
payload = {"alias": new_name}
|
||||
response = client.patch(
|
||||
@@ -397,9 +359,9 @@ class TestProviderViewSet:
|
||||
],
|
||||
)
|
||||
def test_providers_partial_update_invalid_fields(
|
||||
self, client, providers, tenant_header, attribute_key, attribute_value
|
||||
self, client, providers_fixture, tenant_header, attribute_key, attribute_value
|
||||
):
|
||||
provider1, *_ = providers
|
||||
provider1, *_ = providers_fixture
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "Provider",
|
||||
@@ -415,15 +377,15 @@ class TestProviderViewSet:
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
def test_providers_delete(self, client, providers, tenant_header):
|
||||
provider1, *_ = providers
|
||||
def test_providers_delete(self, client, providers_fixture, tenant_header):
|
||||
provider1, *_ = providers_fixture
|
||||
response = client.delete(
|
||||
reverse("provider-detail", kwargs={"pk": provider1.id}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert "Content-Location" in response.headers
|
||||
assert Provider.objects.count() == len(providers) - 1
|
||||
assert Provider.objects.count() == len(providers_fixture) - 1
|
||||
# TODO Assert a task is returned when they are implemented
|
||||
|
||||
def test_providers_delete_invalid(self, client, tenant_header):
|
||||
@@ -435,14 +397,14 @@ class TestProviderViewSet:
|
||||
|
||||
@patch("tasks.tasks.check_provider_connection_task.delay")
|
||||
def test_providers_connection(
|
||||
self, mock_provider_connection, client, providers, tenant_header
|
||||
self, mock_provider_connection, client, providers_fixture, tenant_header
|
||||
):
|
||||
task_mock = Mock()
|
||||
task_mock.id = "12345"
|
||||
task_mock.status = "PENDING"
|
||||
mock_provider_connection.return_value = task_mock
|
||||
|
||||
provider1, *_ = providers
|
||||
provider1, *_ = providers_fixture
|
||||
assert provider1.connected is None
|
||||
assert provider1.connection_last_checked_at is None
|
||||
|
||||
@@ -458,7 +420,7 @@ class TestProviderViewSet:
|
||||
assert response.headers["Content-Location"] == f"api/v1/tasks/{task_mock.id}"
|
||||
|
||||
def test_providers_connection_invalid_provider(
|
||||
self, client, providers, tenant_header
|
||||
self, client, providers_fixture, tenant_header
|
||||
):
|
||||
response = client.post(
|
||||
reverse("provider-connection", kwargs={"pk": "random_id"}),
|
||||
@@ -528,3 +490,244 @@ class TestProviderViewSet:
|
||||
reverse("provider-list"), {"sort": "invalid"}, headers=tenant_header
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestScanViewSet:
|
||||
def test_scans_list(self, client, scans_fixture, tenant_header):
|
||||
response = client.get(reverse("scan-list"), headers=tenant_header)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()["data"]) == len(scans_fixture)
|
||||
|
||||
def test_scans_retrieve(self, client, scans_fixture, tenant_header):
|
||||
scan1, *_ = scans_fixture
|
||||
response = client.get(
|
||||
reverse("scan-detail", kwargs={"pk": scan1.id}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json()["data"]["attributes"]["name"] == scan1.name
|
||||
assert response.json()["data"]["relationships"]["provider"]["data"][
|
||||
"id"
|
||||
] == str(scan1.provider.id)
|
||||
|
||||
def test_scans_invalid_retrieve(self, client, tenant_header):
|
||||
response = client.get(
|
||||
reverse("scan-detail", kwargs={"pk": "random_id"}),
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scan_json_payload, expected_scanner_args",
|
||||
[
|
||||
# Case 1: No scanner_args in payload (should use provider's scanner_args)
|
||||
(
|
||||
{
|
||||
"data": {
|
||||
"type": "Scan",
|
||||
"attributes": {
|
||||
"name": "New Scan",
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {"type": "Provider", "id": "provider-id-1"}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
{"key1": "value1", "key2": {"key21": "value21"}},
|
||||
),
|
||||
(
|
||||
{
|
||||
"data": {
|
||||
"type": "Scan",
|
||||
"attributes": {
|
||||
"name": "New Scan",
|
||||
"scanner_args": {
|
||||
"key2": {"key21": "test21"},
|
||||
"key3": "test3",
|
||||
},
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {"type": "Provider", "id": "provider-id-1"}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
{"key1": "value1", "key2": {"key21": "test21"}, "key3": "test3"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scans_create_valid(
|
||||
self,
|
||||
client,
|
||||
tenant_header,
|
||||
scan_json_payload,
|
||||
expected_scanner_args,
|
||||
providers_fixture,
|
||||
):
|
||||
*_, provider5 = providers_fixture
|
||||
# Provider5 has these scanner_args
|
||||
# scanner_args={"key1": "value1", "key2": {"key21": "value21"}}
|
||||
|
||||
scan_json_payload["data"]["relationships"]["provider"]["data"]["id"] = str(
|
||||
provider5.id
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
reverse("scan-list"),
|
||||
data=scan_json_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert Scan.objects.count() == 1
|
||||
|
||||
scan = Scan.objects.get()
|
||||
assert scan.name == scan_json_payload["data"]["attributes"]["name"]
|
||||
assert scan.provider == provider5
|
||||
assert scan.type == Scan.TypeChoices.MANUAL
|
||||
assert scan.scanner_args == expected_scanner_args
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scan_json_payload, error_code",
|
||||
[
|
||||
(
|
||||
{
|
||||
"data": {
|
||||
"type": "Scan",
|
||||
"attributes": {
|
||||
"name": "a",
|
||||
"type": Scan.TypeChoices.MANUAL,
|
||||
},
|
||||
"relationships": {
|
||||
"provider": {
|
||||
"data": {"type": "Provider", "id": "provider-id-1"}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"min_length",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scans_invalid_create(
|
||||
self, client, tenant_header, scan_json_payload, providers_fixture, error_code
|
||||
):
|
||||
provider1, *_ = providers_fixture
|
||||
scan_json_payload["data"]["relationships"]["provider"]["data"]["id"] = str(
|
||||
provider1.id
|
||||
)
|
||||
response = client.post(
|
||||
reverse("scan-list"),
|
||||
data=scan_json_payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.json()["errors"][0]["code"] == error_code
|
||||
assert (
|
||||
response.json()["errors"][0]["source"]["pointer"] == "/data/attributes/name"
|
||||
)
|
||||
|
||||
def test_scans_partial_update(self, client, scans_fixture, tenant_header):
|
||||
scan1, *_ = scans_fixture
|
||||
new_name = "Updated Scan Name"
|
||||
payload = {
|
||||
"data": {
|
||||
"type": "Scan",
|
||||
"id": scan1.id,
|
||||
"attributes": {"name": new_name},
|
||||
},
|
||||
}
|
||||
response = client.patch(
|
||||
reverse("scan-detail", kwargs={"pk": scan1.id}),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
scan1.refresh_from_db()
|
||||
assert scan1.name == new_name
|
||||
|
||||
def test_scans_partial_update_invalid_content_type(
|
||||
self, client, scans_fixture, tenant_header
|
||||
):
|
||||
scan1, *_ = scans_fixture
|
||||
response = client.patch(
|
||||
reverse("scan-detail", kwargs={"pk": scan1.id}),
|
||||
data={},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
|
||||
|
||||
def test_scans_partial_update_invalid_content(
|
||||
self, client, scans_fixture, tenant_header
|
||||
):
|
||||
scan1, *_ = scans_fixture
|
||||
new_name = "Updated Scan Name"
|
||||
payload = {"name": new_name}
|
||||
response = client.patch(
|
||||
reverse("scan-detail", kwargs={"pk": scan1.id}),
|
||||
data=payload,
|
||||
content_type=API_JSON_CONTENT_TYPE,
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_name, filter_value",
|
||||
[
|
||||
("provider", "aws"),
|
||||
("type", Scan.TypeChoices.MANUAL),
|
||||
("name", "Scan 1"),
|
||||
("started_at", "2024-01-01 00:00:00"),
|
||||
],
|
||||
)
|
||||
def test_scans_filters(self, client, tenant_header, filter_name, filter_value):
|
||||
response = client.get(
|
||||
reverse("scan-list"),
|
||||
{f"filter[{filter_name}]": filter_value},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_name",
|
||||
[
|
||||
"provider", # Valid filter, invalid value
|
||||
"invalid",
|
||||
],
|
||||
)
|
||||
def test_scans_filters_invalid(self, client, tenant_header, filter_name):
|
||||
response = client.get(
|
||||
reverse("scan-list"),
|
||||
{f"filter[{filter_name}]": "invalid_value"},
|
||||
headers=tenant_header,
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sort_field",
|
||||
[
|
||||
"provider_id",
|
||||
"name",
|
||||
"type",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
],
|
||||
)
|
||||
def test_scans_sort(self, client, tenant_header, sort_field):
|
||||
response = client.get(
|
||||
reverse("scan-list"), {"sort": sort_field}, headers=tenant_header
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
def test_scans_sort_invalid(self, client, tenant_header):
|
||||
response = client.get(
|
||||
reverse("scan-list"), {"sort": "invalid"}, headers=tenant_header
|
||||
)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
@@ -0,0 +1,25 @@
|
||||
def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
|
||||
"""
|
||||
Recursively merge two dictionaries, using `default_dict` as the base and `replacement_dict` for overriding values.
|
||||
|
||||
Args:
|
||||
default_dict (dict): The base dictionary containing default key-value pairs.
|
||||
replacement_dict (dict): The dictionary containing values that should override those in `default_dict`.
|
||||
|
||||
Returns:
|
||||
dict: A new dictionary containing all keys from `default_dict` with values from `replacement_dict` replacing
|
||||
any overlapping keys. If a key in both `default_dict` and `replacement_dict` contains dictionaries,
|
||||
this function will merge them recursively.
|
||||
"""
|
||||
result = default_dict.copy()
|
||||
|
||||
for key, value in replacement_dict.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
if value:
|
||||
result[key] = merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
@@ -2,8 +2,9 @@ from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework_json_api import serializers
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.models import Provider
|
||||
from api.models import StateChoices, Provider, Scan
|
||||
from api.rls import Tenant
|
||||
from api.utils import merge_dicts
|
||||
|
||||
|
||||
# Base
|
||||
@@ -31,6 +32,12 @@ class RLSSerializer(BaseSerializerV1):
|
||||
return super().create(validated_data)
|
||||
|
||||
|
||||
class StateEnumSerializerField(serializers.ChoiceField):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["choices"] = StateChoices.choices
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
# Tasks
|
||||
|
||||
|
||||
@@ -59,7 +66,7 @@ class TenantSerializer(BaseSerializerV1):
|
||||
|
||||
|
||||
# Providers
|
||||
class ProviderEnumField(serializers.ChoiceField):
|
||||
class ProviderEnumSerializerField(serializers.ChoiceField):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["choices"] = Provider.ProviderChoices.choices
|
||||
super().__init__(**kwargs)
|
||||
@@ -70,7 +77,7 @@ class ProviderSerializer(RLSSerializer):
|
||||
Serializer for the Provider model.
|
||||
"""
|
||||
|
||||
provider = ProviderEnumField(choices=Provider.ProviderChoices.choices)
|
||||
provider = ProviderEnumSerializerField()
|
||||
connection = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@@ -84,13 +91,8 @@ class ProviderSerializer(RLSSerializer):
|
||||
"alias",
|
||||
"connection",
|
||||
"scanner_args",
|
||||
"url",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
"inserted_at": {"read_only": True},
|
||||
"updated_at": {"read_only": True},
|
||||
"connection": {"read_only": True},
|
||||
}
|
||||
|
||||
@extend_schema_field(
|
||||
{
|
||||
@@ -108,6 +110,12 @@ class ProviderSerializer(RLSSerializer):
|
||||
}
|
||||
|
||||
|
||||
class ProviderCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ["alias", "provider", "provider_id", "scanner_args"]
|
||||
|
||||
|
||||
class ProviderUpdateSerializer(BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for updating the Provider model.
|
||||
@@ -117,3 +125,79 @@ class ProviderUpdateSerializer(BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = ["alias", "scanner_args"]
|
||||
|
||||
|
||||
# Scans
|
||||
|
||||
|
||||
class ScanTypeEnumSerializerField(serializers.ChoiceField):
|
||||
def __init__(self, **kwargs):
|
||||
kwargs["choices"] = Scan.TypeChoices.choices
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class ScanSerializer(RLSSerializer):
|
||||
type_ = serializers.ChoiceField(choices=Scan.TypeChoices.choices, read_only=True)
|
||||
state = StateEnumSerializerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Scan
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"type_",
|
||||
"state",
|
||||
"unique_resource_count",
|
||||
"progress",
|
||||
"scanner_args",
|
||||
"duration",
|
||||
"provider",
|
||||
"started_at",
|
||||
"completed_at",
|
||||
"scheduled_at",
|
||||
"url",
|
||||
]
|
||||
|
||||
def get_fields(self):
|
||||
"""`type` is a Python reserved keyword."""
|
||||
fields = super().get_fields()
|
||||
type_ = fields.pop("type_")
|
||||
fields["type"] = type_
|
||||
return fields
|
||||
|
||||
|
||||
class ScanCreateSerializer(RLSSerializer, BaseWriteSerializer):
|
||||
class Meta:
|
||||
model = Scan
|
||||
# TODO: add mutelist when implemented
|
||||
fields = ["provider", "scanner_args", "name"]
|
||||
|
||||
def create(self, validated_data):
|
||||
provider = validated_data.get("provider")
|
||||
|
||||
if not validated_data.get("scanner_args"):
|
||||
validated_data["scanner_args"] = provider.scanner_args
|
||||
else:
|
||||
validated_data["scanner_args"] = merge_dicts(
|
||||
provider.scanner_args, validated_data["scanner_args"]
|
||||
)
|
||||
|
||||
if not validated_data.get("type"):
|
||||
validated_data["type"] = Scan.TypeChoices.MANUAL.value
|
||||
|
||||
return RLSSerializer.create(self, validated_data)
|
||||
|
||||
|
||||
class ScanUpdateSerializer(BaseWriteSerializer):
|
||||
"""
|
||||
Serializer for updating the Provider model.
|
||||
Only allows "alias" and "scanner_args" fields to be updated.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Scan
|
||||
# TODO: add mutelist when implemented
|
||||
fields = ["id", "name"]
|
||||
extra_kwargs = {
|
||||
"id": {"read_only": True},
|
||||
}
|
||||
@@ -2,12 +2,13 @@ from django.urls import path, include
|
||||
from drf_spectacular.views import SpectacularRedocView
|
||||
from rest_framework import routers
|
||||
|
||||
from api.v1.views import SchemaView, TenantViewSet, ProviderViewSet
|
||||
from api.v1.views import SchemaView, TenantViewSet, ProviderViewSet, ScanViewSet
|
||||
|
||||
router = routers.DefaultRouter(trailing_slash=False)
|
||||
|
||||
router.register(r"tenants", TenantViewSet, basename="tenant")
|
||||
router.register(r"providers", ProviderViewSet, basename="provider")
|
||||
router.register(r"scans", ScanViewSet, basename="scan")
|
||||
|
||||
urlpatterns = [
|
||||
path("", include(router.urls)),
|
||||
|
||||
+106
-8
@@ -1,4 +1,5 @@
|
||||
from django.conf import settings as django_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_control
|
||||
from drf_spectacular.settings import spectacular_settings
|
||||
@@ -10,14 +11,18 @@ from rest_framework.generics import get_object_or_404
|
||||
from rest_framework_json_api.views import Response
|
||||
|
||||
from api.base_views import BaseRLSViewSet, BaseViewSet
|
||||
from api.filters import ProviderFilter, TenantFilter
|
||||
from api.models import Provider
|
||||
from api.filters import ProviderFilter, TenantFilter, ScanFilter
|
||||
from api.models import Provider, Scan
|
||||
from api.rls import Tenant
|
||||
from api.v1.serializers import (
|
||||
ProviderSerializer,
|
||||
ProviderCreateSerializer,
|
||||
ProviderUpdateSerializer,
|
||||
TenantSerializer,
|
||||
DelayedTaskSerializer,
|
||||
ScanSerializer,
|
||||
ScanCreateSerializer,
|
||||
ScanUpdateSerializer,
|
||||
)
|
||||
from tasks.tasks import check_provider_connection_task
|
||||
|
||||
@@ -106,6 +111,7 @@ class TenantViewSet(BaseViewSet):
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
class ProviderViewSet(BaseRLSViewSet):
|
||||
queryset = Provider.objects.all()
|
||||
serializer_class = ProviderSerializer
|
||||
http_method_names = ["get", "post", "patch", "delete"]
|
||||
filterset_class = ProviderFilter
|
||||
search_fields = ["provider", "provider_id", "alias"]
|
||||
@@ -123,17 +129,25 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
return Provider.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
# TODO Add `destroy` when refactored
|
||||
if self.action in {"connection"}:
|
||||
return DelayedTaskSerializer
|
||||
return ProviderSerializer
|
||||
if self.action == "create":
|
||||
return ProviderCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return ProviderUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = ProviderUpdateSerializer(instance, data=request.data, partial=True)
|
||||
serializer = self.get_serializer(
|
||||
instance,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context=self.get_serializer_context(),
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
read_serializer = ProviderSerializer(instance)
|
||||
read_serializer = ProviderSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
)
|
||||
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@extend_schema(
|
||||
@@ -168,3 +182,87 @@ class ProviderViewSet(BaseRLSViewSet):
|
||||
**response.headers,
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
summary="List all scans",
|
||||
description="Retrieve a list of all scans with options for filtering by various criteria.",
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
summary="Retrieve data from a specific scan",
|
||||
description="Fetch detailed information about a specific scan by its ID.",
|
||||
),
|
||||
create=extend_schema(
|
||||
summary="Trigger a manual scan",
|
||||
description=(
|
||||
"Trigger a manual scan by providing the required scan details. "
|
||||
"If `scanner_args` are not provided, the system will automatically use the default settings "
|
||||
"from the associated provider. If you do provide `scanner_args`, these settings will be "
|
||||
"merged with the provider's defaults. This means that your provided settings will override "
|
||||
"the defaults only where they conflict, while the rest of the default settings will remain intact."
|
||||
),
|
||||
),
|
||||
partial_update=extend_schema(
|
||||
summary="Partially update a scan",
|
||||
description="Update certain fields of an existing scan without affecting other fields.",
|
||||
request=ScanUpdateSerializer,
|
||||
responses={200: ScanSerializer},
|
||||
),
|
||||
)
|
||||
@method_decorator(CACHE_DECORATOR, name="list")
|
||||
@method_decorator(CACHE_DECORATOR, name="retrieve")
|
||||
class ScanViewSet(BaseRLSViewSet):
|
||||
queryset = Scan.objects.all()
|
||||
serializer_class = ScanSerializer
|
||||
http_method_names = ["get", "post", "patch"]
|
||||
filterset_class = ScanFilter
|
||||
ordering = ["inserted_at"]
|
||||
ordering_fields = [
|
||||
"provider_id",
|
||||
"name",
|
||||
"type",
|
||||
"attempted_at",
|
||||
"scheduled_at",
|
||||
"inserted_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
return Scan.objects.all()
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == "create":
|
||||
return ScanCreateSerializer
|
||||
elif self.action == "partial_update":
|
||||
return ScanUpdateSerializer
|
||||
return super().get_serializer_class()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
scan = serializer.save()
|
||||
|
||||
# TODO: Run scan through task and return task info here
|
||||
return Response(
|
||||
data=serializer.data,
|
||||
status=status.HTTP_202_ACCEPTED,
|
||||
headers={
|
||||
"Content-Location": reverse("scan-detail", kwargs={"pk": scan.id})
|
||||
},
|
||||
)
|
||||
|
||||
def partial_update(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(
|
||||
instance,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context=self.get_serializer_context(),
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
read_serializer = ScanSerializer(
|
||||
instance, context=self.get_serializer_context()
|
||||
)
|
||||
return Response(data=read_serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -151,3 +151,6 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
# Cache settings
|
||||
CACHE_MAX_AGE = env.int("DJANGO_CACHE_MAX_AGE", 3600)
|
||||
CACHE_STALE_WHILE_REVALIDATE = env.int("DJANGO_STALE_WHILE_REVALIDATE", 60)
|
||||
|
||||
|
||||
TESTING = False
|
||||
@@ -7,7 +7,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["*"])
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"prowler_user": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": env("POSTGRES_DB", default="prowler_db"),
|
||||
"USER": env("POSTGRES_USER", default="prowler_user"),
|
||||
@@ -24,7 +24,7 @@ DATABASES = {
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
},
|
||||
}
|
||||
|
||||
DATABASES["default"] = DATABASES["prowler_user"]
|
||||
|
||||
REST_FRAMEWORK["DEFAULT_RENDERER_CLASSES"] = tuple( # noqa: F405
|
||||
render_class
|
||||
|
||||
@@ -8,7 +8,7 @@ ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.
|
||||
# Database
|
||||
# TODO Use Django database routers https://docs.djangoproject.com/en/5.0/topics/db/multi-db/#automatic-database-routing
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"prowler_user": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": env("POSTGRES_DB"),
|
||||
"USER": env("POSTGRES_USER"),
|
||||
@@ -25,3 +25,4 @@ DATABASES = {
|
||||
"PORT": env("POSTGRES_PORT"),
|
||||
},
|
||||
}
|
||||
DATABASES["default"] = DATABASES["prowler_user"]
|
||||
@@ -5,6 +5,7 @@ from config.env import env
|
||||
DEBUG = env.bool("DJANGO_DEBUG", default=False)
|
||||
ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["localhost", "127.0.0.1"])
|
||||
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
@@ -13,7 +14,8 @@ DATABASES = {
|
||||
"PASSWORD": env("POSTGRES_PASSWORD", default="S3cret"),
|
||||
"HOST": env("POSTGRES_HOST", default="localhost"),
|
||||
"PORT": env("POSTGRES_PORT", default="5432"),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = []
|
||||
TESTING = True
|
||||
+106
-18
@@ -1,9 +1,42 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.db import connections as django_connections
|
||||
from rest_framework import status
|
||||
|
||||
from api.models import Provider, Scan, StateChoices
|
||||
from api.rls import Tenant
|
||||
|
||||
API_JSON_CONTENT_TYPE = "application/vnd.api+json"
|
||||
# TODO Change to 401 when authentication/authorization is implemented
|
||||
NO_TENANT_HTTP_STATUS = status.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def enforce_test_user_db_connection(django_db_setup, django_db_blocker):
|
||||
"""Ensure tests use the test user for database connections."""
|
||||
with django_db_blocker.unblock():
|
||||
test_user = "test"
|
||||
test_password = "test"
|
||||
original_user = settings.DATABASES["default"]["USER"]
|
||||
original_password = settings.DATABASES["default"]["PASSWORD"]
|
||||
|
||||
django_connections["default"].settings_dict["USER"] = test_user
|
||||
django_connections["default"].settings_dict["PASSWORD"] = test_password
|
||||
|
||||
django_connections["default"].close()
|
||||
django_connections["default"].connect()
|
||||
|
||||
yield
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
django_connections["default"].settings_dict["USER"] = original_user
|
||||
django_connections["default"].settings_dict["PASSWORD"] = original_password
|
||||
|
||||
django_connections["default"].close()
|
||||
django_connections["default"].connect()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_logging():
|
||||
@@ -11,32 +44,87 @@ def disable_logging():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_tenant():
|
||||
tenant = Tenant.objects.create(
|
||||
def tenants_fixture():
|
||||
tenant1 = Tenant.objects.create(
|
||||
name="Tenant One",
|
||||
inserted_at="2023-01-01T00:00:00Z",
|
||||
updated_at="2023-01-02T00:00:00Z",
|
||||
)
|
||||
return tenant
|
||||
tenant2 = Tenant.objects.create(
|
||||
name="Tenant Two",
|
||||
inserted_at="2023-01-03T00:00:00Z",
|
||||
updated_at="2023-01-04T00:00:00Z",
|
||||
)
|
||||
return tenant1, tenant2
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_header(get_tenant):
|
||||
return {"X-Tenant-ID": str(get_tenant.id)}
|
||||
def providers_fixture(tenants_fixture):
|
||||
tenant, _ = tenants_fixture
|
||||
provider1 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789012",
|
||||
alias="aws_testing_1",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider2 = Provider.objects.create(
|
||||
provider="aws",
|
||||
provider_id="123456789013",
|
||||
alias="aws_testing_2",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider3 = Provider.objects.create(
|
||||
provider="gcp",
|
||||
provider_id="a12322-test321",
|
||||
alias="gcp_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider4 = Provider.objects.create(
|
||||
provider="kubernetes",
|
||||
provider_id="kubernetes-test-12345",
|
||||
alias="k8s_testing",
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
provider5 = Provider.objects.create(
|
||||
provider="azure",
|
||||
provider_id="37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
alias="azure_testing",
|
||||
tenant_id=tenant.id,
|
||||
scanner_args={"key1": "value1", "key2": {"key21": "value21"}},
|
||||
)
|
||||
|
||||
return provider1, provider2, provider3, provider4, provider5
|
||||
|
||||
|
||||
# Put this in your conftest.py
|
||||
@pytest.fixture(scope="session")
|
||||
def celery_config():
|
||||
return {
|
||||
"broker_url": "redis://",
|
||||
"result_backend": "redis://",
|
||||
"broker_connection_retry_on_startup": True,
|
||||
}
|
||||
@pytest.fixture
|
||||
def scans_fixture(tenants_fixture, providers_fixture):
|
||||
tenant, _ = tenants_fixture
|
||||
provider, *_ = providers_fixture
|
||||
|
||||
scan1 = Scan.objects.create(
|
||||
name="Scan 1",
|
||||
provider=provider,
|
||||
type=Scan.TypeChoices.MANUAL,
|
||||
state=StateChoices.AVAILABLE,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
scan2 = Scan.objects.create(
|
||||
name="Scan 2",
|
||||
provider=provider,
|
||||
type=Scan.TypeChoices.SCHEDULED,
|
||||
state=StateChoices.FAILED,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
scan3 = Scan.objects.create(
|
||||
name="Scan 3",
|
||||
provider=provider,
|
||||
type=Scan.TypeChoices.SCHEDULED,
|
||||
state=StateChoices.AVAILABLE,
|
||||
tenant_id=tenant.id,
|
||||
)
|
||||
return scan1, scan2, scan3
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def celery_includes():
|
||||
return [
|
||||
"tasks",
|
||||
]
|
||||
@pytest.fixture
|
||||
def tenant_header(tenants_fixture):
|
||||
return {"X-Tenant-ID": str(tenants_fixture[0].id)}
|
||||
@@ -17,8 +17,8 @@ from tasks.jobs.connection import check_provider_connection
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db
|
||||
def test_check_provider_connection(get_tenant, provider_data, provider_class):
|
||||
provider = Provider.objects.create(**provider_data, tenant_id=get_tenant.id)
|
||||
def test_check_provider_connection(tenants_fixture, provider_data, provider_class):
|
||||
provider = Provider.objects.create(**provider_data, tenant_id=tenants_fixture[0].id)
|
||||
|
||||
mock_test_connection_result = MagicMock()
|
||||
mock_test_connection_result.is_connected = True
|
||||
|
||||
Reference in new issue
Block a user