-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: rename email subscription user IDs to lowercase
We had a customer report where they complained that they were still receiving Errata notifications even after unsubscribing from them. After investigating, we found out that we had multiple and even overlapping email subscriptions for almost the same username. By "almost", I mean that the username was essentially the same, but with different case. For example: UserA and usera. Notifications takes all the email subscriptions' usernames and makes them lowercase to make a case insensitive comparison. Also, when user preferences are saved, the email subscriptions that are generated are generated with lowercase user IDs. When we migrated the Errata email subscriptions, some of them contained mixed case user IDs, and we did not realize about that. What ended up happening was: - A customer was initially subscribed to errata Notifications, and therefore we migrated their email subscription from Errata with a mixed case. - The customer unsubscribed, which generated a lowercase email unsubscription. - The recipients resolver took both subscriptions and since it still found the mixed case subscription, it was sending the email. RHCLOUD-37310
- Loading branch information
1 parent
0a6d105
commit b583052
Showing
6 changed files
with
453 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
...t/cloud/notifications/routers/internal/errata/ErrataEmailPreferencesUserIdFixService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package com.redhat.cloud.notifications.routers.internal.errata; | ||
|
||
import com.redhat.cloud.notifications.Constants; | ||
import com.redhat.cloud.notifications.db.repositories.SubscriptionRepository; | ||
import com.redhat.cloud.notifications.models.EventTypeEmailSubscription; | ||
import io.quarkus.logging.Log; | ||
import jakarta.enterprise.context.ApplicationScoped; | ||
import jakarta.inject.Inject; | ||
import jakarta.ws.rs.POST; | ||
import jakarta.ws.rs.Path; | ||
|
||
import java.util.List; | ||
import java.util.Set; | ||
|
||
@ApplicationScoped | ||
@Path(Constants.API_INTERNAL + "/team-nado") | ||
public class ErrataEmailPreferencesUserIdFixService { | ||
@Inject | ||
SubscriptionRepository subscriptionRepository; | ||
|
||
@Path("/migrate/rename-lowercase") | ||
@POST | ||
public void migrateMixedCaseEmailSubscriptions() { | ||
final Set<String> mixedCaseUserIds = this.subscriptionRepository.findMixedCaseUserIds(); | ||
|
||
Log.debugf("Fetched the following email subscription user IDs: %s", mixedCaseUserIds); | ||
|
||
for (final String mixedCaseUserId : mixedCaseUserIds) { | ||
Log.debugf("[user_id: %s] Processing mixed case user id", mixedCaseUserId); | ||
|
||
final List<EventTypeEmailSubscription> mixedCaseEmailSubs = this.subscriptionRepository.findEmailSubscriptionsByUserId(mixedCaseUserId); | ||
|
||
Log.debugf("[user_id: %s] Fetched the following email subscriptions for user: %s", mixedCaseUserId, mixedCaseEmailSubs); | ||
|
||
for (final EventTypeEmailSubscription subscription : mixedCaseEmailSubs) { | ||
this.subscriptionRepository.setEmailSubscriptionUserIdLowercase(subscription); | ||
} | ||
} | ||
} | ||
} |
205 changes: 205 additions & 0 deletions
205
.../test/java/com/redhat/cloud/notifications/db/repositories/SubscriptionRepositoryTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
package com.redhat.cloud.notifications.db.repositories; | ||
|
||
import com.redhat.cloud.notifications.config.BackendConfig; | ||
import com.redhat.cloud.notifications.db.DbIsolatedTest; | ||
import com.redhat.cloud.notifications.db.ResourceHelpers; | ||
import com.redhat.cloud.notifications.models.Application; | ||
import com.redhat.cloud.notifications.models.Bundle; | ||
import com.redhat.cloud.notifications.models.EventType; | ||
import com.redhat.cloud.notifications.models.EventTypeEmailSubscription; | ||
import com.redhat.cloud.notifications.models.SubscriptionType; | ||
import io.quarkus.test.InjectMock; | ||
import io.quarkus.test.junit.QuarkusTest; | ||
import jakarta.inject.Inject; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mockito; | ||
|
||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Random; | ||
import java.util.Set; | ||
|
||
import static com.redhat.cloud.notifications.TestConstants.DEFAULT_ORG_ID; | ||
|
||
@QuarkusTest | ||
public class SubscriptionRepositoryTest extends DbIsolatedTest { | ||
@InjectMock | ||
BackendConfig backendConfig; | ||
|
||
@Inject | ||
ResourceHelpers resourceHelpers; | ||
|
||
@Inject | ||
SubscriptionRepository subscriptionRepository; | ||
|
||
/** | ||
* Tests that the function under test only finds mixed cased user IDs from | ||
* email subscriptions. | ||
*/ | ||
@Test | ||
void testFindMixedCaseUserIds() { | ||
// Simulate that the default template is enabled so that we can create | ||
// instant subscriptions without facing any "missing template" errors. | ||
Mockito.when(this.backendConfig.isDefaultTemplateEnabled()).thenReturn(true); | ||
|
||
// Create a single event type we want to create the subscriptions for. | ||
final Bundle randomBundle = this.resourceHelpers.createBundle("random-bundle"); | ||
final Application randomApplication = this.resourceHelpers.createApplication(randomBundle.getId(), "random-application"); | ||
final EventType randomEventType = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type"); | ||
|
||
// Create five subscriptions with mixed case user IDs. | ||
final Random random = new Random(); | ||
final Set<String> expectedUsernames = new HashSet<>(5); | ||
for (int i = 0; i < 5; i++) { | ||
final String userId; | ||
if (random.nextBoolean()) { | ||
userId = String.format("MixedCaseUSERid%d", i); | ||
} else { | ||
userId = String.format("UPPERCASEUSERID%d", i); | ||
} | ||
final String orgId = String.format("%d", i); | ||
|
||
// Store the created user ID to verify it afterward. | ||
expectedUsernames.add(userId); | ||
|
||
this.subscriptionRepository.updateSubscription(orgId, userId, randomEventType.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
} | ||
|
||
// Create five subscriptions with lowercase usernames. | ||
for (int i = 0; i < 5; i++) { | ||
final String userId = String.format("lowercaseusername%d", i); | ||
final String orgId = String.format("%d", i); | ||
|
||
this.subscriptionRepository.updateSubscription(orgId, userId, randomEventType.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
} | ||
|
||
// Call the function under test. | ||
final Set<String> userIds = this.subscriptionRepository.findMixedCaseUserIds(); | ||
|
||
// Assert that the fetched user IDs are just the ones with mixed case. | ||
for (final String userId : userIds) { | ||
Assertions.assertTrue( | ||
expectedUsernames.contains(userId), | ||
String.format("a non-mixed-case user ID \"%s\" was fetched from the database", userId) | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Tests that the function under test only finds the email subscriptions | ||
* related to the given user ID. | ||
*/ | ||
@Test | ||
void testFindEmailSubscriptionsByUserId() { | ||
// Simulate that the default template is enabled so that we can create | ||
// instant subscriptions without facing any "missing template" errors. | ||
Mockito.when(this.backendConfig.isDefaultTemplateEnabled()).thenReturn(true); | ||
|
||
// Create a single event type we want to create the subscriptions for. | ||
final Bundle randomBundle = this.resourceHelpers.createBundle("random-bundle"); | ||
final Application randomApplication = this.resourceHelpers.createApplication(randomBundle.getId(), "random-application"); | ||
final EventType randomEventType = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type"); | ||
final EventType randomEventTypeTwo = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type-two"); | ||
final EventType randomEventTypeThree = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type-three"); | ||
|
||
// Create some subscriptions for two different users. | ||
final String userIdOne = "userIdOne"; | ||
final String userIdTwo = "userIdTwo"; | ||
|
||
final Random random = new Random(); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdOne, randomEventType.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdOne, randomEventTypeTwo.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdOne, randomEventTypeThree.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
|
||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdTwo, randomEventType.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdTwo, randomEventTypeTwo.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userIdTwo, randomEventTypeThree.getId(), SubscriptionType.INSTANT, random.nextBoolean()); | ||
|
||
// Call the function under test. | ||
final List<EventTypeEmailSubscription> subscriptions = this.subscriptionRepository.findEmailSubscriptionsByUserId(userIdOne); | ||
|
||
// Assert that all the subscriptions belong to the target user ID. | ||
for (final EventTypeEmailSubscription subscription : subscriptions) { | ||
Assertions.assertEquals(userIdOne, subscription.getUserId(), "fetched a subscription from a different user"); | ||
} | ||
} | ||
|
||
/** | ||
* Tests that an email subscription's user ID can be set to lowercase, and | ||
* that when it is not possible due to an already existing email | ||
* subscription, the email subscription gets deleted. | ||
*/ | ||
@Test | ||
void testSetSubscriptionUserIdLowercase() { | ||
// Simulate that the default template is enabled so that we can create | ||
// instant subscriptions without facing any "missing template" errors. | ||
Mockito.when(this.backendConfig.isDefaultTemplateEnabled()).thenReturn(true); | ||
|
||
// Create a single event type we want to create the subscriptions for. | ||
final Bundle randomBundle = this.resourceHelpers.createBundle("random-bundle"); | ||
final Application randomApplication = this.resourceHelpers.createApplication(randomBundle.getId(), "random-application"); | ||
final EventType randomEventType = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type"); | ||
final EventType randomEventTypeTwo = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type-two"); | ||
final EventType randomEventTypeThree = this.resourceHelpers.createEventType(randomApplication.getId(), "random-event-type-three"); | ||
|
||
// Create some subscriptions for a user that should simply be | ||
// renamed to lowercase. | ||
final String userId = "userIdOne"; | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventType.getId(), SubscriptionType.INSTANT, true); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventTypeTwo.getId(), SubscriptionType.INSTANT, false); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventTypeThree.getId(), SubscriptionType.INSTANT, true); | ||
|
||
// Find the email subscriptions for the user. | ||
final List<EventTypeEmailSubscription> originalSubscriptions = this.subscriptionRepository.findEmailSubscriptionsByUserId(userId); | ||
|
||
// Set the user ID to lowercase. | ||
for (final EventTypeEmailSubscription subscription : originalSubscriptions) { | ||
this.subscriptionRepository.setEmailSubscriptionUserIdLowercase(subscription); | ||
} | ||
|
||
// Attempt finding the email subscriptions again for the original user | ||
// ID. | ||
Assertions.assertEquals( | ||
0, | ||
this.subscriptionRepository.findEmailSubscriptionsByUserId(userId).size(), | ||
String.format("fetched email subscriptions for \"%s\", when their user ID should have been renamed to lowercase", userId) | ||
); | ||
|
||
// Attempt finding the email subscriptions but for the lowercase user | ||
// ID. | ||
Assertions.assertEquals( | ||
3, | ||
this.subscriptionRepository.findEmailSubscriptionsByUserId(userId.toLowerCase()).size(), | ||
String.format("user ID \"%s\" should have had all the subscriptions renamed to lowercase", userId.toLowerCase()) | ||
); | ||
|
||
// Create the same subscriptions again for the previous user ID. | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventType.getId(), SubscriptionType.INSTANT, true); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventTypeTwo.getId(), SubscriptionType.INSTANT, false); | ||
this.subscriptionRepository.updateSubscription(DEFAULT_ORG_ID, userId, randomEventTypeThree.getId(), SubscriptionType.INSTANT, true); | ||
|
||
// Find the email subscriptions for the user again. | ||
final List<EventTypeEmailSubscription> duplicatedMixedCaseSubscriptions = this.subscriptionRepository.findEmailSubscriptionsByUserId(userId); | ||
|
||
// Attempt setting the user ID to lowercase. | ||
for (final EventTypeEmailSubscription subscription : duplicatedMixedCaseSubscriptions) { | ||
this.subscriptionRepository.setEmailSubscriptionUserIdLowercase(subscription); | ||
} | ||
|
||
// Assert that the mixed case subscriptions were deleted. | ||
Assertions.assertEquals( | ||
0, | ||
this.subscriptionRepository.findEmailSubscriptionsByUserId(userId).size(), | ||
String.format("fetched email subscriptions for \"%s\", when the subscriptions themselves should have been deleted", userId) | ||
); | ||
|
||
// Assert that the email subscriptions with the lowercase user ID still | ||
// exist. | ||
Assertions.assertEquals( | ||
3, | ||
this.subscriptionRepository.findEmailSubscriptionsByUserId(userId.toLowerCase()).size(), | ||
String.format("user ID \"%s\" should still have all the lowercase email subscriptions after deleting the mixed case ones", userId.toLowerCase()) | ||
); | ||
} | ||
} |
Oops, something went wrong.