diff --git a/CHANGELOG.md b/CHANGELOG.md index 36e9fb8..1a262dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changelog [Unreleased] ------------ +- Analytics and Monitoring (#52) + [v1.0.2] - 2023-09-16 ------------------ diff --git a/Makefile b/Makefile index 3a867ed..7ce0cfc 100644 --- a/Makefile +++ b/Makefile @@ -22,9 +22,12 @@ format: ${bin}black ${pysources} migrations: + @echo "Running migrations..." ${bin}python -m scripts.makemigrations + migrations-check: + @echo "Running migrations checks..." ${bin}python -m scripts.makemigrations --check test: diff --git a/docs/analytics.rst b/docs/analytics.rst new file mode 100644 index 0000000..09539a7 --- /dev/null +++ b/docs/analytics.rst @@ -0,0 +1,89 @@ +====================== +API Key Analytics Middleware +====================== + +The API Key Analytics Middleware is a component of the `rest_framework_simple_api_key` package that provides +real-time analytics on API key usage. It records each API request, tracking which endpoints are accessed and how frequently. + +API Key Analytics Middleware Usage cases +===================================== + +Here is why you can use the analytics feature in your application: + +- **Enhanced Decision Making**: It provides comprehensive data on API usage patterns, enabling better resource management and optimization decisions. + +- **Accurate Billing**: You can use it for precise billing by tracking each user's API usage, ensuring customers are billed based on actual usage. + +- **Improved Security Oversight**: It monitor API access patterns that can help you quickly detect and respond to unauthorized or suspicious activities. + + +This page details how to integrate and configure the API Key Analytics Middleware in your Django project, +and explains its operation. + +Middleware Overview +------------------- + +The `ApiKeyAnalyticsMiddleware` automatically tracks access to different API endpoints by intercepting API requests. +It logs each access in the database, allowing you to monitor API usage and optimize API key allocations. + +Setup +----- + +To use the `ApiKeyAnalyticsMiddleware`, follow these setup instructions: + +1. Ensure the middleware app `rest_framework_simple_api_key.analytics` is included in the ``INSTALLED_APPS`` setting + of your Django project. + + .. code-block:: python + + INSTALLED_APPS = ( + ... + "rest_framework", + "rest_framework_simple_api_key", + "rest_framework_simple_api_key.analytics", # Ensure this app is added + ) + +2. Add the `ApiKeyAnalyticsMiddleware` to the `MIDDLEWARE` settings in your Django configuration. + + .. code-block:: python + + MIDDLEWARE = [ + ... + 'django.middleware.security.SecurityMiddleware', + 'rest_framework_simple_api_key.analytics.middleware.ApiKeyAnalyticsMiddleware', # Add the middleware here + ... + ] + +3. Run the migrate command to create the necessary database tables: + + .. code-block:: shell + + python manage.py migrate rest_framework_simple_api_key_analytics + +Activation +---------- + +The middleware is activated as soon as it is added to the `MIDDLEWARE` list and the project is restarted. +No further actions are required to start collecting data. + +How the Middleware Works +------------------------ + +Once activated, the middleware performs the following functions: + +1. **Request Interception**: Upon receiving an API request, the middleware extracts the API key used to authenticate the request. + +2. **Endpoint Tracking**: It logs the endpoint accessed by the API key. + +3. **Data Storage**: All access data is stored in the `ApiKeyAnalytics` model, which can be queried to retrieve analytics. + +Data Access +----------- + +To access the analytics data: + +1. Use Django's admin interface to view and manage the data collected by the middleware. + +2. Access the `ApiKeyAnalytics` model through Django's ORM to perform custom queries or export data for further analysis. + + diff --git a/docs/conf.py b/docs/conf.py index fbc1041..7f1db24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = "Django REST Simple Api Key" -copyright = "2023, Kolawole with ❤️" +copyright = "2024, koladev with ❤️" author = "Kolawole Mangabo" @@ -49,15 +49,3 @@ # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinx_rtd_theme" - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 37282fe..d57ca71 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ Contents permissions authentication rotation + analytics development_and_contributing changelog diff --git a/pyproject.toml b/pyproject.toml index 84b6303..3608e4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,4 +6,5 @@ build-backend = "setuptools.build_meta" [project] name = "djangorestframework-simple-apikey" -version = "1.0.2" \ No newline at end of file +version = "1.0.2" +dynamic = ["description", "readme", "optional-dependencies", "dependencies", "classifiers", "authors", "license"] \ No newline at end of file diff --git a/rest_framework_simple_api_key/admin.py b/rest_framework_simple_api_key/admin.py index 6864958..2a72580 100644 --- a/rest_framework_simple_api_key/admin.py +++ b/rest_framework_simple_api_key/admin.py @@ -46,7 +46,7 @@ def save_model( change: bool = False, ) -> None: """ - If there is obj.pk, it means that the object is been created. We need then to display the + If there is obj.pk, it means that the object is being created. We need then to display the `api_key` value in the Django admin dashboard. """ diff --git a/rest_framework_simple_api_key/analytics/__init__.py b/rest_framework_simple_api_key/analytics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rest_framework_simple_api_key/analytics/admin.py b/rest_framework_simple_api_key/analytics/admin.py new file mode 100644 index 0000000..719031d --- /dev/null +++ b/rest_framework_simple_api_key/analytics/admin.py @@ -0,0 +1,23 @@ +from django.contrib import admin +from .models import ApiKeyAnalytics + + +class ApiKeyAnalyticsAdmin(admin.ModelAdmin): + list_display = ("id", "request_number", "accessed_endpoints", "api_key") + + list_filter = ( + "request_number", + "api_key", + ) + + def has_add_permission(self, request): + return False + + def has_change_permission(self, request, obj=None): + return False + + def has_delete_permission(self, request, obj=None): + return False + + +admin.site.register(ApiKeyAnalytics, ApiKeyAnalyticsAdmin) diff --git a/rest_framework_simple_api_key/analytics/apps.py b/rest_framework_simple_api_key/analytics/apps.py new file mode 100644 index 0000000..964e302 --- /dev/null +++ b/rest_framework_simple_api_key/analytics/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class AnalyticsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "rest_framework_simple_api_key.analytics" + label = "rest_framework_simple_api_key_analytics" diff --git a/rest_framework_simple_api_key/analytics/middleware.py b/rest_framework_simple_api_key/analytics/middleware.py new file mode 100644 index 0000000..cba06eb --- /dev/null +++ b/rest_framework_simple_api_key/analytics/middleware.py @@ -0,0 +1,26 @@ +from rest_framework_simple_api_key.analytics.models import ApiKeyAnalytics +from rest_framework_simple_api_key.crypto import get_crypto +from rest_framework_simple_api_key.parser import APIKeyParser +from rest_framework_simple_api_key.utils import get_key + + +class ApiKeyAnalyticsMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Code to be executed for each request before + # the view (and later middleware) are called. + + response = self.get_response(request) + + key = get_key(APIKeyParser(), request) + + payload = get_crypto().decrypt(key) + + # Use the custom manager to handle endpoint access logging + ApiKeyAnalytics.objects.add_endpoint_access( + api_key_id=payload["_pk"], endpoint=request.path + ) + + return response diff --git a/rest_framework_simple_api_key/analytics/migrations/0001_initial.py b/rest_framework_simple_api_key/analytics/migrations/0001_initial.py new file mode 100644 index 0000000..7680c9f --- /dev/null +++ b/rest_framework_simple_api_key/analytics/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated by Django 5.0.6 on 2024-05-22 22:37 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("rest_framework_simple_api_key", "0002_alter_apikey_options"), + ] + + operations = [ + migrations.CreateModel( + name="ApiKeyAnalytics", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("request_number", models.IntegerField(default=0)), + ("accessed_endpoints", models.JSONField(default=dict)), + ( + "api_key", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="analytics", + to="rest_framework_simple_api_key.apikey", + ), + ), + ], + ), + ] diff --git a/rest_framework_simple_api_key/analytics/migrations/__init__.py b/rest_framework_simple_api_key/analytics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rest_framework_simple_api_key/analytics/models.py b/rest_framework_simple_api_key/analytics/models.py new file mode 100644 index 0000000..2fabdd3 --- /dev/null +++ b/rest_framework_simple_api_key/analytics/models.py @@ -0,0 +1,54 @@ +from django.db import models +from rest_framework_simple_api_key.settings import package_settings + + +class ApiKeyAnalyticsManager(models.Manager): + def add_endpoint_access(self, api_key_id, endpoint): + """ + Retrieve or create ApiKeyAnalytics instance and increment the endpoint access count. + """ + obj, created = self.get_or_create( + api_key_id=api_key_id, defaults={"accessed_endpoints": {"endpoints": {}}} + ) + + # Initialize endpoints dictionary if it doesn't exist + if "endpoints" not in obj.accessed_endpoints: + obj.accessed_endpoints["endpoints"] = {} + + # Increment endpoint count + obj.accessed_endpoints["endpoints"][endpoint] = ( + obj.accessed_endpoints["endpoints"].get(endpoint, 0) + 1 + ) + obj.request_number += 1 + obj.save() + + def get_most_accessed_endpoints(self, api_key_id): + """ + Returns the most accessed endpoints for a given API key, sorted by access count. + """ + obj = self.get(api_key_id=api_key_id) + if ( + "endpoints" in obj.accessed_endpoints + and obj.accessed_endpoints["endpoints"] + ): + return sorted( + obj.accessed_endpoints["endpoints"].items(), + key=lambda item: item[1], + reverse=True, + ) + return [] + + +class ApiKeyAnalytics(models.Model): + api_key = models.ForeignKey( + package_settings.API_KEY_CLASS, + on_delete=models.CASCADE, + related_name="analytics", + ) + request_number = models.IntegerField(default=0) + accessed_endpoints = models.JSONField(default=dict) + + objects = ApiKeyAnalyticsManager() + + def __str__(self): + return f"API Key {self.api_key.name} Analytics" diff --git a/rest_framework_simple_api_key/management/commands/rotation.py b/rest_framework_simple_api_key/management/commands/rotation.py index ea1210e..40fdb17 100644 --- a/rest_framework_simple_api_key/management/commands/rotation.py +++ b/rest_framework_simple_api_key/management/commands/rotation.py @@ -8,30 +8,36 @@ class Command(BaseCommand): - help = 'Starts or stops a rotation based on command arguments' + help = "Starts or stops a rotation based on command arguments" def add_arguments(self, parser): parser.add_argument( - '--stop', - action='store_true', - help='Stops the rotation (Default is to start)', + "--stop", + action="store_true", + help="Stops the rotation (Default is to start)", ) def handle(self, *args, **options): - if options['stop']: + if options["stop"]: # Stop the rotation logic try: - rotation = Rotation.objects.filter(is_rotation_enabled=True).latest('started') + rotation = Rotation.objects.filter(is_rotation_enabled=True).latest( + "started" + ) rotation.is_rotation_enabled = False rotation.ended = timezone.now() rotation.save() - self.stdout.write(self.style.SUCCESS('Successfully stopped rotation')) + self.stdout.write(self.style.SUCCESS("Successfully stopped rotation")) except Rotation.DoesNotExist: - raise CommandError('No active rotation found to stop') + raise CommandError("No active rotation found to stop") else: # Start the rotation logic obj = Rotation() obj.is_rotation_enabled = True obj.ended = timezone.now() + package_settings.ROTATION_PERIOD obj.save() - self.stdout.write(self.style.SUCCESS(f'Successfully started rotation ending at {obj.ended}')) + self.stdout.write( + self.style.SUCCESS( + f"Successfully started rotation ending at {obj.ended}" + ) + ) diff --git a/rest_framework_simple_api_key/migrations/0001_initial.py b/rest_framework_simple_api_key/migrations/0001_initial.py index 9276a06..68ac96d 100644 --- a/rest_framework_simple_api_key/migrations/0001_initial.py +++ b/rest_framework_simple_api_key/migrations/0001_initial.py @@ -7,7 +7,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/rest_framework_simple_api_key/migrations/0002_alter_apikey_options.py b/rest_framework_simple_api_key/migrations/0002_alter_apikey_options.py index f5f4dea..a925fc9 100644 --- a/rest_framework_simple_api_key/migrations/0002_alter_apikey_options.py +++ b/rest_framework_simple_api_key/migrations/0002_alter_apikey_options.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("rest_framework_simple_api_key", "0001_initial"), ] diff --git a/rest_framework_simple_api_key/models.py b/rest_framework_simple_api_key/models.py index aaf05e2..d09db13 100644 --- a/rest_framework_simple_api_key/models.py +++ b/rest_framework_simple_api_key/models.py @@ -85,6 +85,9 @@ class Meta: verbose_name = "API key" verbose_name_plural = "API keys" + def __str__(self): + return self.name + class APIKey(AbstractAPIKey): """ diff --git a/rest_framework_simple_api_key/parser.py b/rest_framework_simple_api_key/parser.py index cce13b4..8123e41 100644 --- a/rest_framework_simple_api_key/parser.py +++ b/rest_framework_simple_api_key/parser.py @@ -16,7 +16,6 @@ class APIKeyParser: message = "No API key provided." def get(self, request: HttpRequest) -> typing.Optional[str]: - return self.get_from_authorization(request) def get_from_authorization(self, request: HttpRequest) -> typing.Optional[str]: diff --git a/rest_framework_simple_api_key/permissions.py b/rest_framework_simple_api_key/permissions.py index 78bdaba..fd85893 100644 --- a/rest_framework_simple_api_key/permissions.py +++ b/rest_framework_simple_api_key/permissions.py @@ -13,11 +13,9 @@ class IsActiveEntity(BasePermission): message = "Entity is not active." def has_permission(self, request: HttpRequest, view: typing.Any) -> bool: - return request.user.is_active def has_object_permission( self, request: HttpRequest, view: typing.Any, obj ) -> bool: - return request.user.is_active diff --git a/rest_framework_simple_api_key/rotation/admin.py b/rest_framework_simple_api_key/rotation/admin.py index 30c66ae..f232cbb 100644 --- a/rest_framework_simple_api_key/rotation/admin.py +++ b/rest_framework_simple_api_key/rotation/admin.py @@ -22,7 +22,7 @@ class RotationAdmin(admin.ModelAdmin): ) def get_readonly_fields( - self, request: HttpRequest, obj: Rotation = None + self, request: HttpRequest, obj: Rotation = None ) -> typing.Tuple[str, ...]: fields = ( "started", @@ -32,11 +32,11 @@ def get_readonly_fields( return fields def save_model( - self, - request: HttpRequest, - obj: Rotation, - form: typing.Any = None, - change: bool = False, + self, + request: HttpRequest, + obj: Rotation, + form: typing.Any = None, + change: bool = False, ) -> None: """ If there is obj.pk, it means that the object has been created already. diff --git a/rest_framework_simple_api_key/rotation/migrations/0001_initial.py b/rest_framework_simple_api_key/rotation/migrations/0001_initial.py index 5c972e7..f701de6 100644 --- a/rest_framework_simple_api_key/rotation/migrations/0001_initial.py +++ b/rest_framework_simple_api_key/rotation/migrations/0001_initial.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - initial = True dependencies = [] diff --git a/rest_framework_simple_api_key/rotation/utils.py b/rest_framework_simple_api_key/rotation/utils.py index c957458..5e8b8ce 100644 --- a/rest_framework_simple_api_key/rotation/utils.py +++ b/rest_framework_simple_api_key/rotation/utils.py @@ -8,12 +8,18 @@ def get_rotation_status(): rotation_status = cache.get("rotation_status") - if rotation_status is None: # We should check for 'None' specifically because the cached value could be False + if ( + rotation_status is None + ): # We should check for 'None' specifically because the cached value could be False # Lazy load the Rotation model - Rotation = apps.get_model('rest_framework_simple_api_key_rotation', 'Rotation') + Rotation = apps.get_model("rest_framework_simple_api_key_rotation", "Rotation") # Filter the latest rotation that is enabled - config = Rotation.objects.filter(is_rotation_enabled=True).order_by('-started').first() + config = ( + Rotation.objects.filter(is_rotation_enabled=True) + .order_by("-started") + .first() + ) # If we have a rotation config and its 'ended' date has passed, update it if config and config.ended and config.ended <= timezone.now(): @@ -29,7 +35,9 @@ def get_rotation_status(): cache.set( "rotation_status", rotation_status, - package_settings.ROTATION_PERIOD.total_seconds() if rotation_status else None, + package_settings.ROTATION_PERIOD.total_seconds() + if rotation_status + else None, ) # Cache for the rotation period if true return rotation_status diff --git a/rest_framework_simple_api_key/settings.py b/rest_framework_simple_api_key/settings.py index 9f5f74f..19c9e77 100644 --- a/rest_framework_simple_api_key/settings.py +++ b/rest_framework_simple_api_key/settings.py @@ -14,6 +14,7 @@ "API_KEY_LIFETIME": 365, "AUTHENTICATION_KEYWORD_HEADER": "Api-Key", "ROTATION_PERIOD": timedelta(days=7), + "API_KEY_CLASS": "rest_framework_simple_api_key.Apikey", } REMOVED_SETTINGS = () diff --git a/rest_framework_simple_api_key/utils.py b/rest_framework_simple_api_key/utils.py new file mode 100644 index 0000000..cf3e7e0 --- /dev/null +++ b/rest_framework_simple_api_key/utils.py @@ -0,0 +1,7 @@ +import typing + +from django.http import HttpRequest + + +def get_key(key_parser, request: HttpRequest) -> typing.Optional[str]: + return key_parser.get(request) diff --git a/scripts/makemigrations.py b/scripts/makemigrations.py index 6a52178..6ff023e 100644 --- a/scripts/makemigrations.py +++ b/scripts/makemigrations.py @@ -9,7 +9,6 @@ sys.path.append(str(root)) if __name__ == "__main__": - APP = "rest_framework_simple_api_key" from django.conf import settings @@ -24,6 +23,7 @@ "rest_framework", "rest_framework_simple_api_key", "rest_framework_simple_api_key.rotation", + "rest_framework_simple_api_key.analytics", "tests", ), DATABASES={ @@ -41,5 +41,10 @@ # For available options, see: # https://docs.djangoproject.com/en/3.0/ref/django-admin/#makemigrations - options = sys.argv[1:] - call_command("makemigrations", *options, APP) + if len(sys.argv) > 1: + app_labels = sys.argv[1:] + call_command("makemigrations", *app_labels) + else: + print( + "No app label provided. Usage: python -m scripts.makemigrations [app_label]" + ) diff --git a/tests/conftest.py b/tests/conftest.py index 92b368a..aee85b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,7 @@ def pytest_configure(): "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", + "rest_framework_simple_api_key.analytics.middleware.ApiKeyAnalyticsMiddleware", ) apps = [ @@ -22,7 +23,8 @@ def pytest_configure(): "django.contrib.staticfiles", "rest_framework", "rest_framework_simple_api_key", - "tests" + "rest_framework_simple_api_key.analytics", + "tests", ] if os.environ.get("TEST_WITH_ROTATION"): diff --git a/tests/fixtures/api_key.py b/tests/fixtures/api_key.py index e278639..1d74d09 100644 --- a/tests/fixtures/api_key.py +++ b/tests/fixtures/api_key.py @@ -25,6 +25,16 @@ def active_api_key(user): ) # This will return api_key:object, key:string@pytest.fixture +@pytest.fixture +def active_only_api_key(user): + data = { + "entity": user, + } + apikey, _ = APIKey.objects.create_api_key(**data) # This will return api_key:object + + return apikey + + @pytest.fixture def expired_api_key(user): data = {"entity": user, "expiry_date": now()} diff --git a/tests/test_analytics.py b/tests/test_analytics.py new file mode 100644 index 0000000..7f08731 --- /dev/null +++ b/tests/test_analytics.py @@ -0,0 +1,52 @@ +import pytest +from rest_framework_simple_api_key.analytics.models import ApiKeyAnalytics + +from .fixtures.api_key import active_only_api_key +from .fixtures.user import user + + +@pytest.fixture +def api_key_analytics(user, active_only_api_key, db): + return ApiKeyAnalytics.objects.create(api_key=active_only_api_key, request_number=0) + + +@pytest.mark.django_db +def test_add_endpoint_access(api_key_analytics, user, active_only_api_key): + endpoint = "/api/data" + ApiKeyAnalytics.objects.add_endpoint_access( + api_key_id=active_only_api_key.id, endpoint=endpoint + ) + + # Fetch the updated object + analytics = ApiKeyAnalytics.objects.get(api_key=active_only_api_key) + assert endpoint in analytics.accessed_endpoints["endpoints"] + assert analytics.accessed_endpoints["endpoints"][endpoint] == 1 + + # Test incrementing the count + ApiKeyAnalytics.objects.add_endpoint_access( + api_key_id=active_only_api_key.id, endpoint=endpoint + ) + analytics = ApiKeyAnalytics.objects.get(api_key=active_only_api_key) + assert analytics.accessed_endpoints["endpoints"][endpoint] == 2 + + +@pytest.mark.django_db +def test_get_most_accessed_endpoints(api_key_analytics, user, active_only_api_key): + endpoints = ["/api/data", "/api/info", "/api/data"] + for endpoint in endpoints: + ApiKeyAnalytics.objects.add_endpoint_access( + api_key_id=active_only_api_key.id, endpoint=endpoint + ) + + most_accessed = ApiKeyAnalytics.objects.get_most_accessed_endpoints( + api_key_id=active_only_api_key.id + ) + assert most_accessed[0][0] == "/api/data" # Most accessed endpoint + assert most_accessed[0][1] == 2 # Accessed twice + + +@pytest.mark.django_db +def test_str(api_key_analytics, user, active_only_api_key): + assert ( + str(api_key_analytics) == f"API Key {api_key_analytics.api_key.name} Analytics" + ) diff --git a/tests/test_analytics_middleware.py b/tests/test_analytics_middleware.py new file mode 100644 index 0000000..75fd5a6 --- /dev/null +++ b/tests/test_analytics_middleware.py @@ -0,0 +1,52 @@ +from unittest import mock + +import pytest +from django.http import HttpResponse +from django.test import RequestFactory +from rest_framework_simple_api_key.analytics.middleware import ApiKeyAnalyticsMiddleware +from rest_framework_simple_api_key.analytics.models import ApiKeyAnalytics +from .fixtures.api_key import active_api_key +from .fixtures.user import user + + +@pytest.fixture +def api_key_analytics(user, active_api_key, db): + apikey, _ = active_api_key + return ApiKeyAnalytics.objects.create(api_key=apikey, request_number=0) + + +@pytest.fixture +def middleware(): + """Return an instance of the middleware.""" + return ApiKeyAnalyticsMiddleware(get_response=lambda request: HttpResponse()) + + +@pytest.fixture +def request_factory(): + return RequestFactory() + + +@pytest.mark.django_db +def test_api_key_analytics_middleware( + middleware, user, active_api_key, request_factory +): + apikey, key = active_api_key + # Create a mock request + request = request_factory.get("/some-path") + + # Assume get_key and get_crypto are properly mocked to return expected values + with mock.patch( + "rest_framework_simple_api_key.parser.APIKeyParser.get", return_value=key + ), mock.patch( + "rest_framework_simple_api_key.crypto.ApiCrypto.decrypt", + return_value={"_pk": apikey.pk}, + ): + # Call middleware + response = middleware(request) + + # Check the response + assert response.status_code == 200 + + # Check if ApiKeyAnalytics was updated + analytics = ApiKeyAnalytics.objects.get(api_key_id=apikey.pk) + assert "/some-path" in analytics.accessed_endpoints["endpoints"] diff --git a/tests/test_api_key_multi_crypto.py b/tests/test_api_key_multi_crypto.py index af52e73..2768cee 100644 --- a/tests/test_api_key_multi_crypto.py +++ b/tests/test_api_key_multi_crypto.py @@ -9,10 +9,10 @@ @pytest.mark.django_db class TestCryptoFunctions: - - @pytest.fixture(scope='class') + @pytest.fixture(scope="class") def key_crypto(self): from rest_framework_simple_api_key.crypto import get_crypto + return get_crypto() def test_encryption_and_decryption(self, key_crypto):