feat/PRWLR-3999 Implement providers endpoints (#21)

* feat(Backend): PRWLR-3989 add RLS to postgresql db and base models

* feat(API): PRWLR-3989 add TenantMiddleware

* chore(API, Backend): PRWLR-3989 create new db user without RLS bypass on migrations

* chore(Backend): PRWLR-3989 fix RLS bypasser for POST requests

* fix(Backend): PRWLR-3989 fix user permissions when migrating new models

* chore(Backend): PRWLR-3989 add testing view for RLS manual tests

* feat(API): PRWLR-3989 add tenant_id to API logging

* chore(API, Backend): PRWLR-3989 add TODOs

* test(API): PRWLR-3989 add new middleware unit tests

* chore(API): PRWLR-3989 refactor RLS code

* fix(tests): PRWLR-3989 fix testing db connector

* chore: PRWLR-3989 add references to JIRA tickets

* fix: PRWLR-3989 remove bypass logic and fix serializers

* fix: PRWLR-3989 improve drop SQL query for RLS models

* feat(Backend): PRWLR-3989 add specific permissions on each model

* fix(Backend): PRWLR-3989 fix database routing and grant select perms

* fix(test): PRWLR-3989 fix routing issues with unit tests

* chore: PRWLR-3989 remove references to JIRA tickets

* feat(Backend): PRWLR-3999 add Provider model

* feat: PRWLR-3999 add providers view logic

* fix: PRWLR-3999 fix unique index fields

* feat(API): PRWLR-3999 add custom exception handlers

* feat(API): PRWLR-3999 add /providers/{provider_id}/connection endpoint

* test(API): PRWLR-3999 add base unit tests for providers

* fix(API, Backend): PRWLR-3999 fix bugs after rebase

* chore(API, docker): PRWLR-3999 add dev feats for demo

* feat(API): PRWLR-3999 add drf-spectacular-jsonapi and improve docs

* test(API): PRWLR-3999 add providers unit tests

* chore(Backend): PRWLR-3989 adjust privileges for user on table tenant

* chore: PRWLR-3999 rename model custom validation error

* chore: PRWLR-3999 remove Test references

* chore: PRWLR-3999 update API v1 spec

* fix: PRWLR-3999 apply requested changes to filter and models

* feat: PRWLR-3999 add validation to PATCH /providers payload

* fix: PRWLR-3999 fix providers enum description

* chore: add more providers fixtures

* fix: PRWLR-3999 make providers.alias optional
This commit is contained in:
Víctor Fernández Poyatos authored and GitHub committed 2024-08-06 12:39:08 -04:00
1 parent e1a12bcb14
commit 4371ad1535
19 files changed
+1888 -481

No files matched your search

+1 -2
View File
@@ -4,7 +4,6 @@ from django.db.migrations.recorder import MigrationRecorder
from django.db.utils import ConnectionRouter
from api.db_router import MainRouter
from api.models import Test
from api.rls import Tenant
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
@@ -17,7 +16,7 @@ class TestMainDatabaseRouter:
yield ConnectionRouter()
settings.DATABASE_ROUTERS = testing_routers
@pytest.mark.parametrize("api_model", [Tenant, Test])
@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
+346 -2
View File
@@ -1,10 +1,15 @@
from datetime import datetime
import pytest
from django.urls import reverse
from rest_framework import status
from api.models import Provider
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.mark.django_db
@@ -50,8 +55,7 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["name"] == tenant1.name
def test_tenants_invalid_retrieve(self, client, tenants):
tenant1, _ = tenants
def test_tenants_invalid_retrieve(self, client):
response = client.get(reverse("tenant-detail", kwargs={"pk": "random_id"}))
assert response.status_code == status.HTTP_404_NOT_FOUND
@@ -177,3 +181,343 @@ class TestTenantViewSet:
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == 2
assert response.json()["data"][0]["attributes"]["name"] == tenant2.name
@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):
response = client.get(reverse("provider-list"), headers=tenant_header)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["data"]) == len(providers)
def test_providers_retrieve(self, client, providers, tenant_header):
provider1, *_ = providers
response = client.get(
reverse("provider-detail", kwargs={"pk": provider1.id}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["data"]["attributes"]["provider"] == provider1.provider
assert (
response.json()["data"]["attributes"]["provider_id"]
== provider1.provider_id
)
assert response.json()["data"]["attributes"]["alias"] == provider1.alias
def test_providers_invalid_retrieve(self, client, tenant_header):
response = client.get(
reverse("provider-detail", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.parametrize(
"provider_json_payload",
(
[
{"provider": "aws", "provider_id": "111111111111", "alias": "test"},
{"provider": "gcp", "provider_id": "a12322-test54321", "alias": "test"},
{
"provider": "kubernetes",
"provider_id": "kubernetes-test-123456789",
"alias": "test",
},
{
"provider": "azure",
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e875",
"alias": "test",
},
]
),
)
def test_providers_create_valid(self, client, tenant_header, provider_json_payload):
response = client.post(
reverse("provider-list"),
data=provider_json_payload,
format="json",
headers=tenant_header,
)
assert response.status_code == status.HTTP_201_CREATED
assert Provider.objects.count() == 1
assert Provider.objects.get().provider == provider_json_payload["provider"]
assert (
Provider.objects.get().provider_id == provider_json_payload["provider_id"]
)
assert Provider.objects.get().alias == provider_json_payload["alias"]
@pytest.mark.parametrize(
"provider_json_payload, error_code",
(
[
(
{"provider": "aws", "provider_id": "1", "alias": "test"},
"min_length",
),
(
{
"provider": "aws",
"provider_id": "1111111111111",
"alias": "test",
},
"aws-provider-id",
),
(
{"provider": "aws", "provider_id": "aaaaaaaaaaaa", "alias": "test"},
"aws-provider-id",
),
(
{"provider": "gcp", "provider_id": "1234asdf", "alias": "test"},
"gcp-provider-id",
),
(
{
"provider": "kubernetes",
"provider_id": "-1234asdf",
"alias": "test",
},
"kubernetes-provider-id",
),
(
{
"provider": "azure",
"provider_id": "8851db6b-42e5-4533-aa9e-30a32d67e87",
"alias": "test",
},
"azure-provider-id",
),
]
),
)
def test_providers_invalid_create(
self, client, tenant_header, provider_json_payload, error_code
):
response = client.post(
reverse("provider-list"),
data=provider_json_payload,
format="json",
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/provider_id"
)
def test_providers_partial_update(self, client, providers, tenant_header):
provider1, *_ = providers
new_alias = "This is the new name"
payload = {
"data": {
"type": "Provider",
"id": provider1.id,
"attributes": {"alias": new_alias},
},
}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_200_OK
provider1.refresh_from_db()
assert provider1.alias == new_alias
def test_providers_partial_update_invalid_content_type(
self, client, providers, tenant_header
):
provider1, *_ = providers
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data={},
headers=tenant_header,
)
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
def test_providers_partial_update_invalid_content(
self, client, providers, tenant_header
):
provider1, *_ = providers
new_name = "This is the new name"
payload = {"alias": new_name}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.parametrize(
"attribute_key, attribute_value",
[
("provider", "aws"),
("provider_id", "123456789012"),
],
)
def test_providers_partial_update_invalid_fields(
self, client, providers, tenant_header, attribute_key, attribute_value
):
provider1, *_ = providers
payload = {
"data": {
"type": "Provider",
"id": provider1.id,
"attributes": {attribute_key: attribute_value},
},
}
response = client.patch(
reverse("provider-detail", kwargs={"pk": provider1.id}),
data=payload,
content_type=API_JSON_CONTENT_TYPE,
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_providers_delete(self, client, providers, tenant_header):
provider1, *_ = providers
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
# TODO Assert a task is returned when they are implemented
def test_providers_delete_invalid(self, client, tenant_header):
response = client.delete(
reverse("provider-detail", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
def test_providers_connection(self, client, providers, tenant_header):
provider1, *_ = providers
assert provider1.connected is None
assert provider1.connection_last_checked_at is None
response = client.post(
reverse("provider-connection", kwargs={"pk": provider1.id}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_202_ACCEPTED
assert "Content-Location" in response.headers
# TODO Assert a task is returned when they are implemented
provider1.refresh_from_db()
assert provider1.connected is True
assert isinstance(provider1.connection_last_checked_at, datetime)
def test_providers_connection_invalid_provider(
self, client, providers, tenant_header
):
response = client.post(
reverse("provider-connection", kwargs={"pk": "random_id"}),
headers=tenant_header,
)
assert response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.parametrize(
"filter_name, filter_value",
(
[
("provider", "aws"),
("provider_id", "12345"),
("alias", "test"),
("search", "test"),
("inserted_at", "2024-01-01 00:00:00"),
("updated_at", "2024-01-01 00:00:00"),
]
),
)
def test_providers_filters(self, client, tenant_header, filter_name, filter_value):
response = client.get(
reverse("provider-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_providers_filters_invalid(self, client, tenant_header, filter_name):
response = client.get(
reverse("provider-list"),
{f"filter[{filter_name}]": "whatever"},
headers=tenant_header,
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@pytest.mark.parametrize(
"sort_field",
(
[
"provider",
"provider_id",
"alias",
"connected",
"inserted_at",
"updated_at",
]
),
)
def test_providers_sort(self, client, tenant_header, sort_field):
response = client.get(
reverse("provider-list"), {"sort": sort_field}, headers=tenant_header
)
assert response.status_code == status.HTTP_200_OK
def test_providers_sort_invalid(self, client, tenant_header):
response = client.get(
reverse("provider-list"), {"sort": "invalid"}, headers=tenant_header
)
assert response.status_code == status.HTTP_400_BAD_REQUEST