From 85eff8afaf5245d849331612b604b4e163a45029 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 2 Jun 2026 18:58:06 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=91=E2=80=8D=F0=9F=92=BB(backend)=20ad?= =?UTF-8?q?d=20email=20filter=20to=20target=20subset=20of=20users=20for=20?= =?UTF-8?q?merge=20cmd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an email substring filter to the user merge management command, allowing selection of a subset of users concerned by the merge process. This enables safer incremental execution of the command by testing it on a controlled group of users before applying it globally. The goal is to validate behavior and ensure the merge process does not introduce unexpected side effects or inconsistencies at scale. --- .../commands/merge_duplicate_users.py | 15 +++++++-- .../test_management_merge_duplicate_users.py | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/backend/core/management/commands/merge_duplicate_users.py b/src/backend/core/management/commands/merge_duplicate_users.py index cc7750d4..a61a13f1 100644 --- a/src/backend/core/management/commands/merge_duplicate_users.py +++ b/src/backend/core/management/commands/merge_duplicate_users.py @@ -36,17 +36,28 @@ class Command(BaseCommand): action="store_true", help="Simulate the merge without writing any changes to the database.", ) + parser.add_argument( + "--email-filter", + type=str, + default=None, + help="Only merge users whose email contains this string (e.g. '@example.com').", + ) def handle(self, *args, **options): """Execute the management command.""" dry_run = options["dry_run"] + email_filter = options["email_filter"] if dry_run: self.stdout.write("[DRY-RUN] No changes will be written.\n") + users_qs = User.objects.all() + if email_filter: + users_qs = users_qs.filter(email__icontains=email_filter) + self.stdout.write(f"[INFO] Filtering emails containing '{email_filter}'.\n") + duplicate_emails = ( - User.objects.all() - .exclude(email__isnull=True) + users_qs.exclude(email__isnull=True) .exclude(email="") .values("email") .annotate(cnt=Count("id")) diff --git a/src/backend/core/tests/management/test_management_merge_duplicate_users.py b/src/backend/core/tests/management/test_management_merge_duplicate_users.py index fe1d6a6d..d29767d7 100644 --- a/src/backend/core/tests/management/test_management_merge_duplicate_users.py +++ b/src/backend/core/tests/management/test_management_merge_duplicate_users.py @@ -427,3 +427,34 @@ def test_merge_is_atomic_rolls_back_all_on_any_failure(mock_reassign_files): for f in files: f.refresh_from_db() assert f.creator == user1 + + +# ── Email filter ─────────────────────────────────────────────────────────────── + + +def test_merge_email_filter_only_merges_matching_emails(): + """Command should only merge users whose email matches the filter.""" + UserFactory(email="user1@example.com") + UserFactory(email="user1@example.com") + other1 = UserFactory(email="user1@other.com") + other2 = UserFactory(email="user1@other.com") + call_command("merge_duplicate_users", email_filter="@example.com") + assert User.objects.filter(email="user1@example.com").count() == 1 + assert User.objects.filter(id=other1.id).exists() + assert User.objects.filter(id=other2.id).exists() + + +def test_merge_email_filter_no_match_does_nothing(): + """Command should do nothing when the email filter matches no users.""" + UserFactory(email="user1@example.com") + UserFactory(email="user1@example.com") + call_command("merge_duplicate_users", email_filter="@nomatch.com") + assert User.objects.filter(email="user1@example.com").count() == 2 + + +def test_merge_email_filter_is_case_insensitive(): + """Command should match emails case-insensitively when filtering.""" + UserFactory(email="user1@Example.com") + UserFactory(email="user1@Example.com") + call_command("merge_duplicate_users", email_filter="@example.com") + assert User.objects.filter(email="user1@Example.com").count() == 1