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:
Víctor Fernández Poyatos authored and GitHub committed 2024-08-30 14:32:13 -04:00
1 parent 8183207802
commit 24857eaa7f
23 files changed
+1379 -409

No files matched your search

@@ -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
+4 -6
View File
@@ -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"
+76
View File
@@ -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
View File
@@ -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