Skip to content

Commit 65419f2

Browse files
authored
Add code to test for an user being added to a service they are already in (#2540)
1 parent c229a76 commit 65419f2

File tree

2 files changed

+128
-9
lines changed

2 files changed

+128
-9
lines changed

app/dao/services_dao.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from notifications_utils.clients.redis import service_cache_key
88
from notifications_utils.statsd_decorators import statsd
99
from notifications_utils.timezones import convert_utc_to_local_timezone
10+
from sqlalchemy.exc import IntegrityError
1011
from sqlalchemy.orm import joinedload
1112
from sqlalchemy.sql.expression import and_, asc, case, func
1213

@@ -41,6 +42,7 @@
4142
Service,
4243
ServicePermission,
4344
ServiceSmsSender,
45+
ServiceUser,
4446
Template,
4547
TemplateCategory,
4648
TemplateHistory,
@@ -348,17 +350,71 @@ def dao_add_user_to_service(service, user, permissions=None, folder_permissions=
348350
try:
349351
from app.dao.permissions_dao import permission_dao
350352

351-
service.users.append(user)
352-
permission_dao.set_user_service_permission(user, service, permissions, _commit=False)
353-
db.session.add(service)
354-
355-
service_user = dao_get_service_user(user.id, service.id)
356-
valid_template_folders = dao_get_valid_template_folders_by_id(folder_permissions)
357-
service_user.folders = valid_template_folders
358-
db.session.add(service_user)
359-
353+
# Check if the user is already in the service using no_autoflush
354+
with db.session.no_autoflush: # Prevent autoflush during this check
355+
service_user = ServiceUser.query.filter_by(user_id=user.id, service_id=service.id).one_or_none()
356+
357+
if service_user:
358+
# User is already in the service, just update permissions if needed
359+
try:
360+
# Convert string permissions to actual permission values if needed
361+
permission_dao.set_user_service_permission(user, service, permissions, _commit=False)
362+
except Exception as perm_error:
363+
db.session.rollback()
364+
current_app.logger.error(
365+
f"Error updating permissions for existing user {user.id} on service {service.id}: {str(perm_error)}"
366+
)
367+
# Try again with a fresh session
368+
db.session.close()
369+
service_user = ServiceUser.query.filter_by(user_id=user.id, service_id=service.id).one()
370+
permission_dao.set_user_service_permission(user, service, permissions, _commit=False)
371+
372+
# Update folder permissions if needed
373+
valid_template_folders = dao_get_valid_template_folders_by_id(folder_permissions)
374+
service_user.folders = valid_template_folders
375+
db.session.add(service_user)
376+
else:
377+
# User is not in service yet, create the ServiceUser record explicitly
378+
service_user = ServiceUser(user_id=user.id, service_id=service.id)
379+
db.session.add(service_user)
380+
381+
try:
382+
# Try to flush now to catch any IntegrityError early
383+
# before proceeding with permissions
384+
db.session.flush()
385+
except IntegrityError:
386+
# If we get an IntegrityError here, it means another request
387+
# has already created the service_user record
388+
db.session.rollback()
389+
# Fetch the existing service_user
390+
service_user = ServiceUser.query.filter_by(user_id=user.id, service_id=service.id).one()
391+
current_app.logger.info(f"Recovered from IntegrityError - user {user.id} already exists in service {service.id}")
392+
393+
# At this point we have a valid service_user - either newly created or fetched after IntegrityError
394+
try:
395+
# Create Permission objects from strings if needed
396+
permission_dao.set_user_service_permission(user, service, permissions, _commit=False)
397+
except Exception as perm_error:
398+
# Log the permission error but continue with folder permissions
399+
current_app.logger.error(
400+
f"Error setting permissions for user {user.id} on service {service.id}: {str(perm_error)}"
401+
)
402+
# Don't re-raise the exception - we'll continue with folder permissions
403+
404+
# Add folder permissions
405+
try:
406+
valid_template_folders = dao_get_valid_template_folders_by_id(folder_permissions)
407+
service_user.folders = valid_template_folders
408+
db.session.add(service_user)
409+
except Exception as folder_error:
410+
# Log the folder permission error but don't fail the entire operation
411+
current_app.logger.error(
412+
f"Error setting folder permissions for user {user.id} on service {service.id}: {str(folder_error)}"
413+
)
414+
# Don't re-raise the exception
360415
except Exception as e:
361416
db.session.rollback()
417+
current_app.logger.error(f"Error in dao_add_user_to_service: {str(e)}")
362418
raise e
363419
else:
364420
db.session.commit()

tests/app/dao/test_services_dao.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import uuid
33
from datetime import datetime, timedelta
4+
from unittest.mock import patch
45

56
import pytest
67
from freezegun import freeze_time
@@ -1550,3 +1551,65 @@ def test_sensitive_service(self, notify_db, notify_db_session):
15501551
def test_non_sensitive_service(self, notify_db, notify_db_session):
15511552
sensitive_service = dao_fetch_service_ids_of_sensitive_services()
15521553
assert sensitive_service == []
1554+
1555+
1556+
class TestAddingUsertoExistingService:
1557+
def test_dao_add_user_to_service_handles_integrity_error_by_getting_existing_user(
1558+
self, notify_db_session, sample_service, sample_user
1559+
):
1560+
"""
1561+
Test that when an IntegrityError occurs during user addition (due to race condition),
1562+
the function recovers by fetching the existing user and updating permissions.
1563+
"""
1564+
1565+
# Create a new user to add to the service
1566+
user = User(
1567+
name="Test Integrity User",
1568+
email_address="[email protected]",
1569+
password="password",
1570+
mobile_number="+16502532223",
1571+
)
1572+
save_model_user(user)
1573+
1574+
# Check how many users are in the service before we start
1575+
service_users_before = len(sample_service.users)
1576+
print(f"Service users before adding new user: {service_users_before}")
1577+
1578+
# First create the service user directly to simulate an existing relationship
1579+
service_user = ServiceUser(user_id=user.id, service_id=sample_service.id)
1580+
db.session.add(service_user)
1581+
db.session.commit()
1582+
1583+
# Verify the service_user exists
1584+
service_user_check = ServiceUser.query.filter_by(user_id=user.id, service_id=sample_service.id).first()
1585+
assert service_user_check is not None, "ServiceUser record was not created manually"
1586+
1587+
# Mock the permission dao to avoid the 'str' has no attribute 'user' error
1588+
with patch("app.dao.permissions_dao.permission_dao.set_user_service_permission") as mock_set_permissions:
1589+
# Now test the integrity error handling by attempting to add the user again with different permissions
1590+
# This simulates a race condition where another request has already created the service_user
1591+
new_permissions = ["send_emails", "send_texts"]
1592+
1593+
# Test that dao_add_user_to_service handles the case where the user is already in the service
1594+
dao_add_user_to_service(sample_service, user, permissions=new_permissions)
1595+
1596+
# Verify the mock was called with the correct parameters
1597+
mock_set_permissions.assert_called_once_with(user, sample_service, new_permissions, _commit=False)
1598+
1599+
# Also mock the scenario where an IntegrityError is raised during the function execution
1600+
# Instead of mocking ServiceUser class, we'll mock the flush method to simulate the IntegrityError
1601+
with (
1602+
patch.object(db.session, "flush") as mock_flush,
1603+
patch("app.dao.permissions_dao.permission_dao.set_user_service_permission") as mock_set_permissions,
1604+
):
1605+
# Configure the flush mock to raise IntegrityError
1606+
mock_flush.side_effect = IntegrityError("duplicate key value", params={}, orig=None)
1607+
1608+
# Try adding with different permissions - this should handle the error gracefully
1609+
newer_permissions = ["send_emails", "send_texts", "manage_service"]
1610+
1611+
# This should not raise an exception to the caller
1612+
dao_add_user_to_service(sample_service, user, permissions=newer_permissions)
1613+
1614+
# Verify the mock was called with the correct parameters
1615+
mock_set_permissions.assert_called_with(user, sample_service, newer_permissions, _commit=False)

0 commit comments

Comments
 (0)