From ac5c9ebbbaa461b2fbae3685f9d671925ec1e881 Mon Sep 17 00:00:00 2001 From: Hamzah Ullah Date: Thu, 31 Oct 2024 15:47:18 -0400 Subject: [PATCH] feat: add bff handler context (#577) --- enterprise_access/apps/bffs/__init__.py | 0 enterprise_access/apps/bffs/apps.py | 8 ++ enterprise_access/apps/bffs/context.py | 59 +++++++++++++ enterprise_access/apps/bffs/serializers.py | 30 +++++++ .../apps/bffs/tests/test_context.py | 86 +++++++++++++++++++ enterprise_access/settings/base.py | 1 + 6 files changed, 184 insertions(+) create mode 100644 enterprise_access/apps/bffs/__init__.py create mode 100644 enterprise_access/apps/bffs/apps.py create mode 100644 enterprise_access/apps/bffs/context.py create mode 100644 enterprise_access/apps/bffs/serializers.py create mode 100644 enterprise_access/apps/bffs/tests/test_context.py diff --git a/enterprise_access/apps/bffs/__init__.py b/enterprise_access/apps/bffs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise_access/apps/bffs/apps.py b/enterprise_access/apps/bffs/apps.py new file mode 100644 index 00000000..61987425 --- /dev/null +++ b/enterprise_access/apps/bffs/apps.py @@ -0,0 +1,8 @@ +""" App config for BFFs """ + +from django.apps import AppConfig + + +class BffsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'enterprise_access.apps.bffs' diff --git a/enterprise_access/apps/bffs/context.py b/enterprise_access/apps/bffs/context.py new file mode 100644 index 00000000..ce4cc9ff --- /dev/null +++ b/enterprise_access/apps/bffs/context.py @@ -0,0 +1,59 @@ +""" +HandlerContext for bffs app. +""" +from enterprise_access.apps.bffs import serializers + + +class HandlerContext: + """ + A context object for managing the state throughout the lifecycle of a Backend-for-Frontend (BFF) request. + The `HandlerContext` class stores request information, loaded data, and any errors and warnings + that may occur during the request. + Attributes: + request: The original request object containing information about the incoming HTTP request. + user: The original request user information about hte incoming HTTP request. + data: A dictionary to store data loaded and processed by the handlers. + errors: A list to store errors that occur during request processing. + warnings: A list to store warnings that occur during the request processing. + enterprise_customer_uuid: The enterprise customer the user is associated with. + lms_user_id: The id associated with the authenticated user. + """ + + def __init__(self, request): + """ + Initializes the HandlerContext with request information, route, and optional initial data. + Args: + request: The incoming HTTP request. + """ + self._request = request + self.data = {} # Stores processed data for the response + self.errors = [] # Stores any errors that occur during processing + self.warnings = [] # Stores any warnings that occur during processing + self.enterprise_customer_uuid = None + self.lms_user_id = None + + @property + def request(self): + return self._request + + @property + def user(self): + return self._request.user + + def add_error(self, **kwargs): + """ + Adds an error to the context. + Output fields determined by the ErrorSerializer + """ + serializer = serializers.ErrorSerializer(data=kwargs) + serializer.is_valid(raise_exception=True) + self.errors.append(serializer.data) + + def add_warning(self, **kwargs): + """ + Adds a warning to the context. + Output fields determined by the WarningSerializer + """ + serializer = serializers.WarningSerializer(data=kwargs) + serializer.is_valid(raise_exception=True) + self.warnings.append(serializer.data) diff --git a/enterprise_access/apps/bffs/serializers.py b/enterprise_access/apps/bffs/serializers.py new file mode 100644 index 00000000..0e470e16 --- /dev/null +++ b/enterprise_access/apps/bffs/serializers.py @@ -0,0 +1,30 @@ +""" +Serializers for bffs. +""" +from rest_framework import serializers + + +class BaseBFFMessageSerializer(serializers.Serializer): + """ + Base Serializer for BFF messages. + + Fields: + user_message (str): A user-friendly message. + developer_message (str): A more detailed message for debugging purposes. + """ + developer_message = serializers.CharField() + user_message = serializers.CharField() + + def create(self, validated_data): + return validated_data + + def update(self, instance, validated_data): + return validated_data + + +class ErrorSerializer(BaseBFFMessageSerializer): + pass + + +class WarningSerializer(BaseBFFMessageSerializer): + pass diff --git a/enterprise_access/apps/bffs/tests/test_context.py b/enterprise_access/apps/bffs/tests/test_context.py new file mode 100644 index 00000000..9af115c2 --- /dev/null +++ b/enterprise_access/apps/bffs/tests/test_context.py @@ -0,0 +1,86 @@ +""" +Text for the BFF context +""" +from django.test import RequestFactory, TestCase +from rest_framework.exceptions import ValidationError + +from enterprise_access.apps.api_client.tests.test_constants import DATE_FORMAT_ISO_8601 +from enterprise_access.apps.bffs.context import HandlerContext +from enterprise_access.apps.core.tests.factories import UserFactory +from enterprise_access.utils import _curr_date + + +class TestHandlerContext(TestCase): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.mock_user = UserFactory() + + def test_handler_context_init(self): + request = self.factory.get('sample/api/call') + request.user = self.mock_user + context = HandlerContext(request) + + self.assertEqual(context.request, request) + self.assertEqual(context.user, self.mock_user) + self.assertEqual(context.data, {}) + self.assertEqual(context.errors, []) + self.assertEqual(context.warnings, []) + self.assertEqual(context.enterprise_customer_uuid, None) + self.assertEqual(context.lms_user_id, None) + + def test_handler_context_add_error_serializer(self): + request = self.factory.get('sample/api/call') + request.user = self.mock_user + context = HandlerContext(request) + expected_output = { + "developer_message": "No enterprise uuid associated to the user mock-uuid", + "user_message": "You may not be associated with the enterprise.", + } + # Define kwargs for add_error + arguments = { + **expected_output, + "status": 403 # Add an attribute that is not explicitly defined in the serializer to verify + } + context.add_error( + **arguments + ) + self.assertEqual(expected_output, context.errors[0]) + + def test_handler_context_add_error_serializer_is_valid(self): + request = self.factory.get('sample/api/call') + request.user = self.mock_user + context = HandlerContext(request) + malformed_output = { + "developer_message": "No enterprise uuid associated to the user mock-uuid", + } + with self.assertRaises(ValidationError): + context.add_error(**malformed_output) + + def test_handler_context_add_warning_serializer(self): + request = self.factory.get('sample/api/call') + request.user = self.mock_user + context = HandlerContext(request) + expected_output = { + "developer_message": "Heuristic Expiration", + "user_message": "The data received might be out-dated", + } + # Define kwargs for add_warning + arguments = { + **expected_output, + "status": 113 # Add an attribute that is not explicitly defined in the serializer to verify + } + context.add_warning( + **arguments + ) + self.assertEqual(expected_output, context.warnings[0]) + + def test_handler_context_add_warning_serializer_is_valid(self): + request = self.factory.get('sample/api/call') + request.user = self.mock_user + context = HandlerContext(request) + malformed_output = { + "user_message": "The data received might be out-dated", + } + with self.assertRaises(ValidationError): + context.add_error(**malformed_output) diff --git a/enterprise_access/settings/base.py b/enterprise_access/settings/base.py index f6a7fbd6..4a8383b7 100644 --- a/enterprise_access/settings/base.py +++ b/enterprise_access/settings/base.py @@ -74,6 +74,7 @@ def root(*path_fragments): 'enterprise_access.apps.subsidy_access_policy', 'enterprise_access.apps.content_assignments', 'enterprise_access.apps.enterprise_groups', + 'enterprise_access.apps.bffs', ) INSTALLED_APPS += THIRD_PARTY_APPS