Skip to content

Commit

Permalink
[ENG-4705] add preliminary POST behvaior (#7)
Browse files Browse the repository at this point in the history
Add basic POST behavior for AuthorizedStorageAccounts and ConfiguredStorageAddons
  • Loading branch information
Johnetordoff authored Feb 1, 2024
1 parent cbb2fad commit 8c987b3
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 16 deletions.
73 changes: 62 additions & 11 deletions addon_service/authorized_storage_account/serializers.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
from rest_framework_json_api import serializers
from rest_framework_json_api.relations import (
HyperlinkedRelatedField,
ResourceRelatedField,
HyperlinkedRelatedField,
)
from rest_framework_json_api.utils import get_resource_type_from_model

from addon_service.models import (
AuthorizedStorageAccount,
ConfiguredStorageAddon,
ExternalStorageService,
ExternalCredentials,
ExternalAccount,
InternalUser,
)


RESOURCE_NAME = get_resource_type_from_model(AuthorizedStorageAccount)


class AccountOwnerField(ResourceRelatedField):
def to_internal_value(self, data):
internal_user, _ = InternalUser.objects.get_or_create(user_uri=data["id"])
return internal_user


class ExternalStorageServiceField(ResourceRelatedField):
def to_internal_value(self, data):
external_storage_service, _ = ExternalStorageService.objects.get_or_create(
auth_uri=data["id"],
)
return external_storage_service


class AuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")
account_owner = HyperlinkedRelatedField(

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

# Check if it's a POST request and remove the field as it's not in our FE spec
if "context" in kwargs and kwargs["context"]["request"].method == "POST":
self.fields.pop("configured_storage_addons", None)

url = serializers.HyperlinkedIdentityField(
view_name=f"{RESOURCE_NAME}-detail",
required=False
)
account_owner = AccountOwnerField(
many=False,
queryset=InternalUser.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
)
external_storage_service = ResourceRelatedField(
external_storage_service = ExternalStorageServiceField(
queryset=ExternalStorageService.objects.all(),
many=False,
related_link_view_name=f"{RESOURCE_NAME}-related",
Expand All @@ -32,18 +58,41 @@ class AuthorizedStorageAccountSerializer(serializers.HyperlinkedModelSerializer)
many=True,
queryset=ConfiguredStorageAddon.objects.all(),
related_link_view_name=f"{RESOURCE_NAME}-related",
required=False,
)
username = serializers.CharField(
write_only=True
) # placeholder for ExternalCredentials integrity only not auth
password = serializers.CharField(
write_only=True
) # placeholder for ExternalCredentials integrity only not auth

included_serializers = {
"account_owner": "addon_service.serializers.InternalUserSerializer",
"external_storage_service": (
"addon_service.serializers.ExternalStorageServiceSerializer"
),
"configured_storage_addons": (
"addon_service.serializers.ConfiguredStorageAddonSerializer"
),
"external_storage_service": "addon_service.serializers.ExternalStorageServiceSerializer",
"configured_storage_addons": "addon_service.serializers.ConfiguredStorageAddonSerializer",
}

def create(self, validate_data):
account_owner = validate_data["account_owner"]
external_storage_service = validate_data["external_storage_service"]
# TODO(ENG-5189): Update this once credentials format is finalized
credentials, created = ExternalCredentials.objects.get_or_create(
oauth_key=validate_data["username"],
oauth_secret=validate_data["password"],
)

external_account, created = ExternalAccount.objects.get_or_create(
owner=account_owner,
credentials=credentials,
credentials_issuer=external_storage_service.credentials_issuer,
)

return AuthorizedStorageAccount.objects.create(
external_storage_service=external_storage_service,
external_account=external_account,
)

class Meta:
model = AuthorizedStorageAccount
fields = [
Expand All @@ -52,4 +101,6 @@ class Meta:
"configured_storage_addons",
"default_root_folder",
"external_storage_service",
"username",
"password",
]
1 change: 0 additions & 1 deletion addon_service/authorized_storage_account/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
class AuthorizedStorageAccountViewSet(ModelViewSet):
queryset = AuthorizedStorageAccount.objects.all()
serializer_class = AuthorizedStorageAccountSerializer
# TODO: permissions_classes
4 changes: 4 additions & 0 deletions addon_service/configured_storage_addon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ class Meta:

class JSONAPIMeta:
resource_name = "configured-storage-addons"

@property
def account_owner(self):
return self.base_account.external_account.owner
12 changes: 10 additions & 2 deletions addon_service/configured_storage_addon/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,26 @@
InternalResource,
)


RESOURCE_NAME = get_resource_type_from_model(ConfiguredStorageAddon)


class AuthorizedResourceField(ResourceRelatedField):
def to_internal_value(self, data):
internal_resource, _ = InternalResource.objects.get_or_create(
resource_uri=data["id"]
)
return internal_resource


class ConfiguredStorageAddonSerializer(serializers.HyperlinkedModelSerializer):
root_folder = serializers.CharField(required=False)
url = serializers.HyperlinkedIdentityField(view_name=f"{RESOURCE_NAME}-detail")
base_account = ResourceRelatedField(
queryset=AuthorizedStorageAccount.objects.all(),
many=False,
related_link_view_name=f"{RESOURCE_NAME}-related",
)
authorized_resource = ResourceRelatedField(
authorized_resource = AuthorizedResourceField(
queryset=InternalResource.objects.all(),
many=False,
related_link_view_name=f"{RESOURCE_NAME}-related",
Expand Down
1 change: 0 additions & 1 deletion addon_service/configured_storage_addon/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@
class ConfiguredStorageAddonViewSet(ModelViewSet):
queryset = ConfiguredStorageAddon.objects.all()
serializer_class = ConfiguredStorageAddonSerializer
# TODO: permissions_classes
1 change: 0 additions & 1 deletion addon_service/credentials_issuer/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from addon_service.common.base_model import AddonsServiceBaseModel


# TODO: consider another name
class CredentialsIssuer(AddonsServiceBaseModel):
name = models.CharField(null=False)

Expand Down
1 change: 1 addition & 0 deletions addon_service/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" Import models here so they auto-detect for makemigrations """

from addon_service.authorized_storage_account.models import AuthorizedStorageAccount
from addon_service.configured_storage_addon.models import ConfiguredStorageAddon
from addon_service.credentials_issuer.models import CredentialsIssuer
Expand Down
1 change: 1 addition & 0 deletions addon_service/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" Import serializers here for convenience """

from addon_service.authorized_storage_account.serializers import (
AuthorizedStorageAccountSerializer,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,44 @@ def test_get_related__several(self):
{_datum["id"] for _datum in _content["data"]},
{str(_addon.pk) for _addon in _addons},
)


class TestAuthorizedStorageAccountPOSTAPI(APITestCase):
@classmethod
def setUpTestData(cls):
cls._ess = _factories.ExternalStorageServiceFactory()
cls._ea = _factories.ExternalAccountFactory()
cls._csa = _factories.ConfiguredStorageAddonFactory()

def test_post(self):
assert not self._ess.authorized_storage_accounts.all() # sanity/factory check

payload = {
"data": {
"type": "authorized-storage-accounts",
"attributes": {
"username": "<placeholder-username>",
"password": "<placeholder-password>",
},
"relationships": {
"external_storage_service": {
"data": {
"type": "external-storage-services",
"id": self._ess.auth_uri,
}
},
"account_owner": {
"data": {
"type": "internal-users",
"id": self._csa.base_account.external_account.owner.user_uri,
}
},
},
}
}

response = self.client.post(
reverse("authorized-storage-accounts-list"), payload, format="vnd.api+json"
)
self.assertEqual(response.status_code, 201)
assert self._ess.authorized_storage_accounts.all()
60 changes: 60 additions & 0 deletions addon_service/tests/test_by_type/test_configured_storage_addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from addon_service.configured_storage_addon.views import ConfiguredStorageAddonViewSet
from addon_service.tests import _factories
from addon_service.tests._helpers import get_test_request
from addon_service.internal_resource.models import InternalResource


class TestConfiguredStorageAddonAPI(APITestCase):
Expand Down Expand Up @@ -145,3 +146,62 @@ def test_get_related(self):
_content["data"]["id"],
str(self._csa.base_account_id),
)


class TestConfiguredStorageAddonPOSTAPI(APITestCase):
@classmethod
def setUpTestData(cls):
cls._asa = _factories.AuthorizedStorageAccountFactory()
cls.default_payload = {
"data": {
"type": "configured-storage-addons",
"relationships": {
"base_account": {
"data": {
"type": "authorized-storage-accounts",
"id": cls._asa.id,
}
},
"authorized_resource": {
"data": {
"type": "internal-resources",
"id": "http://domain.com/test0/",
}
},
},
}
}



def test_post_without_resource(self):
"""
Test for request made without an InternalResource in the system, so one must be created
"""
assert not self._asa.configured_storage_addons.exists() # sanity/factory check


response = self.client.post(
reverse("configured-storage-addons-list"), self.default_payload, format="vnd.api+json"
)
self.assertEqual(response.status_code, 201)
configured_storage_addon = self._asa.configured_storage_addons.first()
assert configured_storage_addon
assert configured_storage_addon.authorized_resource

def test_post_with_resource(self):
"""
Test for request made with a pre-existing InternalResource in the system, don't create one.
"""
assert not self._asa.configured_storage_addons.exists() # sanity/factory check
resource = _factories.InternalResourceFactory()
self.default_payload['data']['relationships']['authorized_resource']['data']['id'] = resource.resource_uri

response = self.client.post(
reverse("configured-storage-addons-list"), self.default_payload, format="vnd.api+json"
)
self.assertEqual(response.status_code, 201)
configured_storage_addon = self._asa.configured_storage_addons.first()
assert configured_storage_addon
assert configured_storage_addon.authorized_resource.resource_uri == resource.resource_uri
assert InternalResource.objects.all().count() == 1
1 change: 1 addition & 0 deletions addon_service/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
""" Import views/viewsets here for convenience """

from addon_service.authorized_storage_account.views import (
AuthorizedStorageAccountViewSet,
)
Expand Down
1 change: 1 addition & 0 deletions app/env.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""settings from environment variables
"""

import os


Expand Down

0 comments on commit 8c987b3

Please sign in to comment.