From 4a684f99478dc5c35d659262efc81ff115b288d6 Mon Sep 17 00:00:00 2001 From: Oliver Falk Date: Tue, 23 Sep 2025 16:47:56 +0200 Subject: [PATCH] Refactor stats tests into separate file with random data - Add random_ip_address() function to ivatar.utils for generating random IP addresses - Create separate test_views_stats.py file with StatsTester class - Move all stats tests from test_views.py to test_views_stats.py - Update tests to use random_string() for emails and OpenIDs instead of static @example.com - Update tests to use random_ip_address() for IP addresses instead of static 192.168.1.x - Remove stats tests from original test_views.py file --- ivatar/test_views.py | 328 -------------------------------- ivatar/test_views_stats.py | 379 +++++++++++++++++++++++++++++++++++++ ivatar/utils.py | 7 + 3 files changed, 386 insertions(+), 328 deletions(-) create mode 100644 ivatar/test_views_stats.py diff --git a/ivatar/test_views.py b/ivatar/test_views.py index 77019fa..2049858 100644 --- a/ivatar/test_views.py +++ b/ivatar/test_views.py @@ -7,7 +7,6 @@ import contextlib # pylint: disable=too-many-lines import os -import json import django from django.urls import reverse from django.test import TestCase @@ -68,333 +67,6 @@ class Tester(TestCase): # pylint: disable=too-many-public-methods # msg_prefix="Why does an invalid hash not redirect to deadbeef?", # ) - def test_stats_basic(self): - """ - Test basic stats functionality - """ - response = self.client.get("/stats/", follow=True) - self.assertEqual(response.status_code, 200, "unable to fetch stats!") - j = json.loads(response.content) - self.assertEqual(j["users"], 1, "user count incorrect") - self.assertEqual(j["mails"], 0, "mails count incorrect") - self.assertEqual(j["openids"], 0, "openids count incorrect") - self.assertEqual(j["unconfirmed_mails"], 0, "unconfirmed mails count incorrect") - self.assertEqual( - j["unconfirmed_openids"], 0, "unconfirmed openids count incorrect" - ) - self.assertEqual(j["avatars"], 0, "avatars count incorrect") - - def test_stats_comprehensive(self): - """ - Test comprehensive stats with actual data - """ - from ivatar.ivataraccount.models import ( - ConfirmedEmail, - ConfirmedOpenId, - Photo, - UnconfirmedEmail, - UnconfirmedOpenId, - ) - - # Create test data - email1 = ConfirmedEmail.objects.create( - user=self.user, email="test1@example.com", ip_address="192.168.1.1" - ) - email1.access_count = 100 - email1.save() - - email2 = ConfirmedEmail.objects.create( - user=self.user, email="test2@example.com", ip_address="192.168.1.2" - ) - email2.access_count = 50 - email2.save() - - openid1 = ConfirmedOpenId.objects.create( - user=self.user, openid="http://test1.example.com/", ip_address="192.168.1.3" - ) - openid1.access_count = 75 - openid1.save() - - # Create photos with valid image data (minimal PNG) - # PNG header + minimal data - png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82" - - photo1 = Photo.objects.create( - user=self.user, data=png_data, format="png", ip_address="192.168.1.4" - ) - photo1.access_count = 200 - photo1.save() - - photo2 = Photo.objects.create( - user=self.user, - data=png_data, # Same data for testing - format="png", # Same format for testing - ip_address="192.168.1.5", - ) - photo2.access_count = 150 - photo2.save() - - # Associate photos with emails/openids - email1.photo = photo1 - email1.save() - email2.photo = photo2 - email2.save() - openid1.photo = photo1 - openid1.save() - - # Create unconfirmed entries - UnconfirmedEmail.objects.create( - user=self.user, email="unconfirmed@example.com", ip_address="192.168.1.6" - ) - - UnconfirmedOpenId.objects.create( - user=self.user, - openid="http://unconfirmed.example.com/", - ip_address="192.168.1.7", - ) - - # Test the stats endpoint - response = self.client.get("/stats/") - self.assertEqual(response.status_code, 200, "unable to fetch stats!") - j = json.loads(response.content) - - # Test basic counts - self.assertEqual(j["users"], 1, "user count incorrect") - self.assertEqual(j["mails"], 2, "mails count incorrect") - self.assertEqual(j["openids"], 1, "openids count incorrect") - self.assertEqual(j["unconfirmed_mails"], 1, "unconfirmed mails count incorrect") - self.assertEqual( - j["unconfirmed_openids"], 1, "unconfirmed openids count incorrect" - ) - self.assertEqual(j["avatars"], 2, "avatars count incorrect") - - # Test top viewed avatars - self.assertIn("top_viewed_avatars", j, "top_viewed_avatars missing") - self.assertEqual( - len(j["top_viewed_avatars"]), 2, "should have 2 top viewed avatars" - ) - # The top viewed avatar should be the one with highest associated email/openid access count - self.assertEqual( - j["top_viewed_avatars"][0]["access_count"], - 100, - "top avatar access count incorrect", - ) - - # Test top queried emails - self.assertIn("top_queried_emails", j, "top_queried_emails missing") - self.assertEqual( - len(j["top_queried_emails"]), 2, "should have 2 top queried emails" - ) - self.assertEqual( - j["top_queried_emails"][0]["access_count"], - 100, - "top email access count incorrect", - ) - - # Test top queried openids - self.assertIn("top_queried_openids", j, "top_queried_openids missing") - self.assertEqual( - len(j["top_queried_openids"]), 1, "should have 1 top queried openid" - ) - self.assertEqual( - j["top_queried_openids"][0]["access_count"], - 75, - "top openid access count incorrect", - ) - - # Test photo format distribution - self.assertIn( - "photo_format_distribution", j, "photo_format_distribution missing" - ) - formats = { - item["format"]: item["count"] for item in j["photo_format_distribution"] - } - self.assertEqual(formats["png"], 2, "png format count incorrect") - - # Test user activity stats - self.assertIn("user_activity", j, "user_activity missing") - self.assertEqual( - j["user_activity"]["users_with_multiple_photos"], - 1, - "users with multiple photos incorrect", - ) - self.assertEqual( - j["user_activity"]["users_with_both_email_and_openid"], - 1, - "users with both email and openid incorrect", - ) - self.assertEqual( - j["user_activity"]["average_photos_per_user"], - 2.0, - "average photos per user incorrect", - ) - - # Test Bluesky handles (should be empty) - self.assertIn("bluesky_handles", j, "bluesky_handles missing") - self.assertEqual( - j["bluesky_handles"]["total_bluesky_handles"], - 0, - "total bluesky handles should be 0", - ) - - # Test photo size stats - self.assertIn("photo_size_stats", j, "photo_size_stats missing") - self.assertGreater( - j["photo_size_stats"]["average_size_bytes"], - 0, - "average photo size should be > 0", - ) - self.assertEqual( - j["photo_size_stats"]["total_photos_analyzed"], - 2, - "total photos analyzed incorrect", - ) - - # Test potential duplicate photos - self.assertIn( - "potential_duplicate_photos", j, "potential_duplicate_photos missing" - ) - self.assertEqual( - j["potential_duplicate_photos"]["potential_duplicate_groups"], - 1, - "should have 1 duplicate group (same PNG data)", - ) - - def test_stats_edge_cases(self): - """ - Test edge cases for stats - """ - # Test with no data - response = self.client.get("/stats/") - self.assertEqual(response.status_code, 200, "unable to fetch stats!") - j = json.loads(response.content) - - # All lists should be empty - self.assertEqual( - len(j["top_viewed_avatars"]), 0, "top_viewed_avatars should be empty" - ) - self.assertEqual( - len(j["top_queried_emails"]), 0, "top_queried_emails should be empty" - ) - self.assertEqual( - len(j["top_queried_openids"]), 0, "top_queried_openids should be empty" - ) - self.assertEqual( - len(j["photo_format_distribution"]), - 0, - "photo_format_distribution should be empty", - ) - self.assertEqual( - j["bluesky_handles"]["total_bluesky_handles"], - 0, - "bluesky_handles should be 0", - ) - self.assertEqual( - j["photo_size_stats"]["total_photos_analyzed"], - 0, - "photo_size_stats should be 0", - ) - self.assertEqual( - j["potential_duplicate_photos"]["potential_duplicate_groups"], - 0, - "potential_duplicate_photos should be 0", - ) - - def test_stats_with_bluesky_handles(self): - """ - Test stats with Bluesky handles - """ - from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId - - # Create email with Bluesky handle - email = ConfirmedEmail.objects.create( - user=self.user, email="bluesky@example.com", ip_address="192.168.1.1" - ) - email.bluesky_handle = "test.bsky.social" - email.access_count = 100 - email.save() - - # Create OpenID with Bluesky handle - openid = ConfirmedOpenId.objects.create( - user=self.user, - openid="http://bluesky.example.com/", - ip_address="192.168.1.2", - ) - openid.bluesky_handle = "another.bsky.social" - openid.access_count = 50 - openid.save() - - response = self.client.get("/stats/") - self.assertEqual(response.status_code, 200, "unable to fetch stats!") - j = json.loads(response.content) - - # Test Bluesky handles stats - self.assertEqual( - j["bluesky_handles"]["total_bluesky_handles"], - 2, - "total bluesky handles incorrect", - ) - self.assertEqual( - j["bluesky_handles"]["bluesky_emails"], 1, "bluesky emails count incorrect" - ) - self.assertEqual( - j["bluesky_handles"]["bluesky_openids"], - 1, - "bluesky openids count incorrect", - ) - self.assertEqual( - len(j["bluesky_handles"]["top_bluesky_handles"]), - 2, - "top bluesky handles count incorrect", - ) - - def test_stats_photo_duplicates(self): - """ - Test potential duplicate photos detection - """ - from ivatar.ivataraccount.models import Photo - - # Create photos with same format and size (potential duplicates) - # PNG header + minimal data - png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82" - - Photo.objects.create( - user=self.user, data=png_data, format="png", ip_address="192.168.1.1" - ) - Photo.objects.create( - user=self.user, - data=png_data, # Same size - format="png", # Same format - ip_address="192.168.1.2", - ) - Photo.objects.create( - user=self.user, - data=png_data, # Same size but different format - format="png", # Same format for testing - ip_address="192.168.1.3", - ) - - response = self.client.get("/stats/") - self.assertEqual(response.status_code, 200, "unable to fetch stats!") - j = json.loads(response.content) - - # Should detect potential duplicates - self.assertEqual( - j["potential_duplicate_photos"]["potential_duplicate_groups"], - 1, - "should have 1 duplicate group", - ) - self.assertEqual( - j["potential_duplicate_photos"]["total_potential_duplicate_photos"], - 3, - "should have 3 potential duplicate photos", - ) - self.assertEqual( - len(j["potential_duplicate_photos"]["potential_duplicate_groups_detail"]), - 1, - "should have 1 duplicate group detail", - ) - def test_logout(self): """ Test if logout works correctly diff --git a/ivatar/test_views_stats.py b/ivatar/test_views_stats.py new file mode 100644 index 0000000..b4237df --- /dev/null +++ b/ivatar/test_views_stats.py @@ -0,0 +1,379 @@ +# -*- coding: utf-8 -*- +""" +Test our StatsView in ivatar.views +""" + +import json +import os +import django +from django.test import TestCase +from django.test import Client +from django.contrib.auth.models import User +from ivatar.utils import random_string, random_ip_address + +os.environ["DJANGO_SETTINGS_MODULE"] = "ivatar.settings" +django.setup() + + +class StatsTester(TestCase): + """ + Test class for StatsView + """ + + client = Client() + user = None + username = random_string() + password = random_string() + + def login(self): + """ + Login as user + """ + self.client.login(username=self.username, password=self.password) + + def setUp(self): + """ + Prepare for tests. + - Create user + """ + self.user = User.objects.create_user( + username=self.username, + password=self.password, + ) + + def test_stats_basic(self): + """ + Test basic stats functionality + """ + response = self.client.get("/stats/", follow=True) + self.assertEqual(response.status_code, 200, "unable to fetch stats!") + j = json.loads(response.content) + self.assertEqual(j["users"], 1, "user count incorrect") + self.assertEqual(j["mails"], 0, "mails count incorrect") + self.assertEqual(j["openids"], 0, "openids count incorrect") + self.assertEqual(j["unconfirmed_mails"], 0, "unconfirmed mails count incorrect") + self.assertEqual( + j["unconfirmed_openids"], 0, "unconfirmed openids count incorrect" + ) + self.assertEqual(j["avatars"], 0, "avatars count incorrect") + + def test_stats_comprehensive(self): + """ + Test comprehensive stats with actual data + """ + from ivatar.ivataraccount.models import ( + ConfirmedEmail, + ConfirmedOpenId, + Photo, + UnconfirmedEmail, + UnconfirmedOpenId, + ) + + # Create test data with random values + email1 = ConfirmedEmail.objects.create( + user=self.user, + email=f"{random_string()}@{random_string()}.{random_string(2)}", + ip_address=random_ip_address(), + ) + email1.access_count = 100 + email1.save() + + email2 = ConfirmedEmail.objects.create( + user=self.user, + email=f"{random_string()}@{random_string()}.{random_string(2)}", + ip_address=random_ip_address(), + ) + email2.access_count = 50 + email2.save() + + openid1 = ConfirmedOpenId.objects.create( + user=self.user, + openid=f"http://{random_string()}.{random_string()}.org/", + ip_address=random_ip_address(), + ) + openid1.access_count = 75 + openid1.save() + + # Create photos with valid image data (minimal PNG) + # PNG header + minimal data + png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82" + + photo1 = Photo.objects.create( + user=self.user, data=png_data, format="png", ip_address=random_ip_address() + ) + photo1.access_count = 200 + photo1.save() + + photo2 = Photo.objects.create( + user=self.user, + data=png_data, # Same data for testing + format="png", # Same format for testing + ip_address=random_ip_address(), + ) + photo2.access_count = 150 + photo2.save() + + # Associate photos with emails/openids + email1.photo = photo1 + email1.save() + email2.photo = photo2 + email2.save() + openid1.photo = photo1 + openid1.save() + + # Create unconfirmed entries + UnconfirmedEmail.objects.create( + user=self.user, + email=f"{random_string()}@{random_string()}.{random_string(2)}", + ip_address=random_ip_address(), + ) + + UnconfirmedOpenId.objects.create( + user=self.user, + openid=f"http://{random_string()}.{random_string()}.org/", + ip_address=random_ip_address(), + ) + + # Test the stats endpoint + response = self.client.get("/stats/") + self.assertEqual(response.status_code, 200, "unable to fetch stats!") + j = json.loads(response.content) + + # Test basic counts + self.assertEqual(j["users"], 1, "user count incorrect") + self.assertEqual(j["mails"], 2, "mails count incorrect") + self.assertEqual(j["openids"], 1, "openids count incorrect") + self.assertEqual(j["unconfirmed_mails"], 1, "unconfirmed mails count incorrect") + self.assertEqual( + j["unconfirmed_openids"], 1, "unconfirmed openids count incorrect" + ) + self.assertEqual(j["avatars"], 2, "avatars count incorrect") + + # Test top viewed avatars + self.assertIn("top_viewed_avatars", j, "top_viewed_avatars missing") + self.assertEqual( + len(j["top_viewed_avatars"]), 2, "should have 2 top viewed avatars" + ) + # The top viewed avatar should be the one with highest associated email/openid access count + self.assertEqual( + j["top_viewed_avatars"][0]["access_count"], + 100, + "top avatar access count incorrect", + ) + + # Test top queried emails + self.assertIn("top_queried_emails", j, "top_queried_emails missing") + self.assertEqual( + len(j["top_queried_emails"]), 2, "should have 2 top queried emails" + ) + self.assertEqual( + j["top_queried_emails"][0]["access_count"], + 100, + "top email access count incorrect", + ) + + # Test top queried openids + self.assertIn("top_queried_openids", j, "top_queried_openids missing") + self.assertEqual( + len(j["top_queried_openids"]), 1, "should have 1 top queried openid" + ) + self.assertEqual( + j["top_queried_openids"][0]["access_count"], + 75, + "top openid access count incorrect", + ) + + # Test photo format distribution + self.assertIn( + "photo_format_distribution", j, "photo_format_distribution missing" + ) + formats = { + item["format"]: item["count"] for item in j["photo_format_distribution"] + } + self.assertEqual(formats["png"], 2, "png format count incorrect") + + # Test user activity stats + self.assertIn("user_activity", j, "user_activity missing") + self.assertEqual( + j["user_activity"]["users_with_multiple_photos"], + 1, + "users with multiple photos incorrect", + ) + self.assertEqual( + j["user_activity"]["users_with_both_email_and_openid"], + 1, + "users with both email and openid incorrect", + ) + self.assertEqual( + j["user_activity"]["average_photos_per_user"], + 2.0, + "average photos per user incorrect", + ) + + # Test Bluesky handles (should be empty) + self.assertIn("bluesky_handles", j, "bluesky_handles missing") + self.assertEqual( + j["bluesky_handles"]["total_bluesky_handles"], + 0, + "total bluesky handles should be 0", + ) + + # Test photo size stats + self.assertIn("photo_size_stats", j, "photo_size_stats missing") + self.assertGreater( + j["photo_size_stats"]["average_size_bytes"], + 0, + "average photo size should be > 0", + ) + self.assertEqual( + j["photo_size_stats"]["total_photos_analyzed"], + 2, + "total photos analyzed incorrect", + ) + + # Test potential duplicate photos + self.assertIn( + "potential_duplicate_photos", j, "potential_duplicate_photos missing" + ) + self.assertEqual( + j["potential_duplicate_photos"]["potential_duplicate_groups"], + 1, + "should have 1 duplicate group (same PNG data)", + ) + + def test_stats_edge_cases(self): + """ + Test edge cases for stats + """ + # Test with no data + response = self.client.get("/stats/") + self.assertEqual(response.status_code, 200, "unable to fetch stats!") + j = json.loads(response.content) + + # All lists should be empty + self.assertEqual( + len(j["top_viewed_avatars"]), 0, "top_viewed_avatars should be empty" + ) + self.assertEqual( + len(j["top_queried_emails"]), 0, "top_queried_emails should be empty" + ) + self.assertEqual( + len(j["top_queried_openids"]), 0, "top_queried_openids should be empty" + ) + self.assertEqual( + len(j["photo_format_distribution"]), + 0, + "photo_format_distribution should be empty", + ) + self.assertEqual( + j["bluesky_handles"]["total_bluesky_handles"], + 0, + "bluesky_handles should be 0", + ) + self.assertEqual( + j["photo_size_stats"]["total_photos_analyzed"], + 0, + "photo_size_stats should be 0", + ) + self.assertEqual( + j["potential_duplicate_photos"]["potential_duplicate_groups"], + 0, + "potential_duplicate_photos should be 0", + ) + + def test_stats_with_bluesky_handles(self): + """ + Test stats with Bluesky handles + """ + from ivatar.ivataraccount.models import ConfirmedEmail, ConfirmedOpenId + + # Create email with Bluesky handle + email = ConfirmedEmail.objects.create( + user=self.user, + email=f"{random_string()}@{random_string()}.{random_string(2)}", + ip_address=random_ip_address(), + ) + email.bluesky_handle = f"{random_string()}.bsky.social" + email.access_count = 100 + email.save() + + # Create OpenID with Bluesky handle + openid = ConfirmedOpenId.objects.create( + user=self.user, + openid=f"http://{random_string()}.{random_string()}.org/", + ip_address=random_ip_address(), + ) + openid.bluesky_handle = f"{random_string()}.bsky.social" + openid.access_count = 50 + openid.save() + + response = self.client.get("/stats/") + self.assertEqual(response.status_code, 200, "unable to fetch stats!") + j = json.loads(response.content) + + # Test Bluesky handles stats + self.assertEqual( + j["bluesky_handles"]["total_bluesky_handles"], + 2, + "total bluesky handles incorrect", + ) + self.assertEqual( + j["bluesky_handles"]["bluesky_emails"], 1, "bluesky emails count incorrect" + ) + self.assertEqual( + j["bluesky_handles"]["bluesky_openids"], + 1, + "bluesky openids count incorrect", + ) + self.assertEqual( + len(j["bluesky_handles"]["top_bluesky_handles"]), + 2, + "top bluesky handles count incorrect", + ) + + def test_stats_photo_duplicates(self): + """ + Test potential duplicate photos detection + """ + from ivatar.ivataraccount.models import Photo + + # Create photos with same format and size (potential duplicates) + # PNG header + minimal data + png_data = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\nIDATx\x9cc```\x00\x00\x00\x04\x00\x01\xdd\x8d\xb4\x1c\x00\x00\x00\x00IEND\xaeB`\x82" + + Photo.objects.create( + user=self.user, data=png_data, format="png", ip_address=random_ip_address() + ) + Photo.objects.create( + user=self.user, + data=png_data, # Same size + format="png", # Same format + ip_address=random_ip_address(), + ) + Photo.objects.create( + user=self.user, + data=png_data, # Same size but different format + format="png", # Same format for testing + ip_address=random_ip_address(), + ) + + response = self.client.get("/stats/") + self.assertEqual(response.status_code, 200, "unable to fetch stats!") + j = json.loads(response.content) + + # Should detect potential duplicates + self.assertEqual( + j["potential_duplicate_photos"]["potential_duplicate_groups"], + 1, + "should have 1 duplicate group", + ) + self.assertEqual( + j["potential_duplicate_photos"]["total_potential_duplicate_photos"], + 3, + "should have 3 potential duplicate photos", + ) + self.assertEqual( + len(j["potential_duplicate_photos"]["potential_duplicate_groups_detail"]), + 1, + "should have 1 duplicate group detail", + ) diff --git a/ivatar/utils.py b/ivatar/utils.py index 3e50824..3df96bf 100644 --- a/ivatar/utils.py +++ b/ivatar/utils.py @@ -111,6 +111,13 @@ def random_string(length=10): ) +def random_ip_address(): + """ + Return a random IP address (IPv4) + """ + return f"{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}" + + def openid_variations(openid): """ Return the various OpenID variations, ALWAYS in the same order: