From 5a01102ab45c669ed710be1008fa0974e8a34d93 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 29 Aug 2023 10:33:48 +0530 Subject: [PATCH 01/12] fix: tests --- .../io/supertokens/test/AuthRecipeTest.java | 35 +++++++++---------- .../accountlinking/api/ActiveUserTest.java | 2 ++ .../api/TestRecipeUserIdInSignInUpAPIs.java | 5 +++ .../api/ThirdPartySignInUpAPITest4_0.java | 2 ++ 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/test/java/io/supertokens/test/AuthRecipeTest.java b/src/test/java/io/supertokens/test/AuthRecipeTest.java index def36c276..d941f81cc 100644 --- a/src/test/java/io/supertokens/test/AuthRecipeTest.java +++ b/src/test/java/io/supertokens/test/AuthRecipeTest.java @@ -439,11 +439,11 @@ public void randomPaginationTest() throws Exception { return; } - Map> signUpMap = getSignUpMap(process); + Map> signUpMap = getSignUpMap(process); - List classes = getUserInfoClassNameList(); + List authRecipes = getAuthRecipes(); - for (String className : classes) { + for (String className : authRecipes) { if (!signUpMap.containsKey(className)) { fail(); } @@ -456,7 +456,7 @@ public void randomPaginationTest() throws Exception { for (int i = 0; i < numberOfUsers; i++) { if (Math.random() > 0.5) { while (true) { - String currUserType = classes.get((int) (Math.random() * classes.size())); + String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size())); AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null); if (user != null) { synchronized (usersCreated) { @@ -468,7 +468,7 @@ public void randomPaginationTest() throws Exception { } else { es.execute(() -> { while (true) { - String currUserType = classes.get((int) (Math.random() * classes.size())); + String currUserType = authRecipes.get((int) (Math.random() * authRecipes.size())); AuthRecipeUserInfo user = signUpMap.get(currUserType).apply(null); if (user != null) { synchronized (usersCreated) { @@ -584,17 +584,17 @@ public void deleteUserTest() throws Exception { return; } - Map> signUpMap = getSignUpMap(process); + Map> signUpMap = getSignUpMap(process); - List classes = getUserInfoClassNameList(); + List authRecipes = getAuthRecipes(); - for (String className : classes) { + for (String className : authRecipes) { if (!signUpMap.containsKey(className)) { fail(); } } - for (String userType : classes) { + for (String userType : authRecipes) { AuthRecipeUserInfo user1 = signUpMap.get(userType).apply(null); JsonObject testMetadata = new JsonObject(); testMetadata.addProperty("test", "test"); @@ -635,19 +635,16 @@ public void deleteUserTest() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } - private static List getUserInfoClassNameList() { - Reflections reflections = new Reflections("io.supertokens"); - Set> classes = reflections.getSubTypesOf(AuthRecipeUserInfo.class); - - return classes.stream().map(Class::getCanonicalName).collect(Collectors.toList()); + private static List getAuthRecipes() { + return Arrays.asList("emailpassword", "thirdparty", "passwordless"); } - private static Map> getSignUpMap( + private static Map> getSignUpMap( TestingProcessManager.TestingProcess process) { AtomicInteger count = new AtomicInteger(); - Map> signUpMap = new HashMap<>(); - signUpMap.put("io.supertokens.pluginInterface.emailpassword.AuthRecipeUserInfo", o -> { + Map> signUpMap = new HashMap<>(); + signUpMap.put("emailpassword", o -> { try { return EmailPassword.signUp(process.getProcess(), "test" + count.getAndIncrement() + "@example.com", "password0"); @@ -655,7 +652,7 @@ private static List getUserInfoClassNameList() { } return null; }); - signUpMap.put("io.supertokens.pluginInterface.thirdparty.AuthRecipeUserInfo", o -> { + signUpMap.put("thirdparty", o -> { try { String thirdPartyId = "testThirdParty"; String thirdPartyUserId = "thirdPartyUserId" + count.getAndIncrement(); @@ -666,7 +663,7 @@ private static List getUserInfoClassNameList() { } return null; }); - signUpMap.put("io.supertokens.pluginInterface.passwordless.AuthRecipeUserInfo", o -> { + signUpMap.put("passwordless", o -> { try { String email = "test" + count.getAndIncrement() + "@example.com"; CreateCodeResponse createCode = Passwordless.createCode(process.getProcess(), email, null, null, null); diff --git a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java index c07765164..3c0a90fd0 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java @@ -128,6 +128,7 @@ public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception { { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -173,6 +174,7 @@ public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception { { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); diff --git a/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java index acbadcf76..b09b69976 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/TestRecipeUserIdInSignInUpAPIs.java @@ -236,6 +236,7 @@ public void testThirdPartySignInUp() throws Exception { { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -255,6 +256,7 @@ public void testThirdPartySignInUp() throws Exception { // Without account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -279,6 +281,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -300,6 +303,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -317,6 +321,7 @@ public void testThirdPartySignInUp() throws Exception { // After account linking JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "facebook"); diff --git a/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java index 09cc93136..290224130 100644 --- a/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java +++ b/src/test/java/io/supertokens/test/thirdparty/api/ThirdPartySignInUpAPITest4_0.java @@ -72,6 +72,7 @@ public void testGoodInput() throws Exception { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); @@ -142,6 +143,7 @@ public void testNotAllowedUpdateOfEmail() throws Exception { JsonObject emailObject = new JsonObject(); emailObject.addProperty("id", "someemail1@gmail.com"); + emailObject.addProperty("isVerified", false); JsonObject signUpRequestBody = new JsonObject(); signUpRequestBody.addProperty("thirdPartyId", "google"); From d62c2fcd95f2dde12b958a289cd9e0b3390a8b3a Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 29 Aug 2023 12:15:22 +0530 Subject: [PATCH 02/12] fix: removed removal of active user --- .../webserver/api/accountlinking/LinkAccountsAPI.java | 2 -- .../supertokens/test/accountlinking/api/ActiveUserTest.java | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java index 10f7a7bd1..76a9d4dfe 100644 --- a/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/accountlinking/LinkAccountsAPI.java @@ -100,8 +100,6 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I AuthRecipe.LinkAccountsResult linkAccountsResult = AuthRecipe.linkAccounts(main, primaryUserIdAppIdentifierWithStorage, recipeUserId, primaryUserId); - // Remove linked account user id from active user - ActiveUsers.removeActiveUser(recipeUserIdAppIdentifierWithStorage, recipeUserId); UserIdMapping.populateExternalUserIdForUsers(primaryUserIdAppIdentifierWithStorage, new AuthRecipeUserInfo[]{linkAccountsResult.user}); JsonObject response = new JsonObject(); diff --git a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java index 3c0a90fd0..10c652c3c 100644 --- a/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/api/ActiveUserTest.java @@ -156,9 +156,9 @@ public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception { WebserverAPI.getLatestCDIVersion().get(), ""); } - // Now there should be only one active user + // we don't remove the active user for the recipe user, so it should still be 2 userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000); - assertEquals(1, userCount); + assertEquals(2, userCount); // Sign in to the accounts once again { @@ -188,7 +188,7 @@ public void testActiveUserIsRemovedAfterLinkingAccounts() throws Exception { // there should still be only one active user userCount = ActiveUsers.countUsersActiveSince(process.getProcess(), System.currentTimeMillis() - 10000); - assertEquals(1, userCount); + assertEquals(2, userCount); process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); From c11e096095737c080d1c015ccf76824c20d3ce00 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 29 Aug 2023 12:44:11 +0530 Subject: [PATCH 03/12] fix: user id mapping deletion --- .../java/io/supertokens/authRecipe/AuthRecipe.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index 4c07bdadf..a2f67c43d 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -834,12 +834,12 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit if (primaryUserIdToDeleteNonAuthRecipe != null) { deleteNonAuthRecipeUser(con, appIdentifierWithStorage, primaryUserIdToDeleteNonAuthRecipe); - - // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the - // previous call to deleteAuthRecipeUser above. - deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), - true); } + + // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the + // previous call to deleteAuthRecipeUser above. + deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), + true); } else { for (LoginMethod lM : userToDelete.loginMethods) { io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = lM.getSupertokensUserId().equals( From 09c2247ca47a012ba1c2226d2d128b703c05c4bd Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 29 Aug 2023 16:23:14 +0530 Subject: [PATCH 04/12] fix: more fixes --- .../io/supertokens/authRecipe/AuthRecipe.java | 9 ++++--- .../multitenancy/Multitenancy.java | 27 +++++++++++++++++++ .../DisassociationNotAllowedException.java | 23 ++++++++++++++++ .../DisassociateUserFromTenant.java | 10 +++++-- .../emailpassword/api/MultitenantAPITest.java | 17 +++++++----- 5 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 src/main/java/io/supertokens/multitenancy/exception/DisassociationNotAllowedException.java diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index a2f67c43d..b90c37c93 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -834,12 +834,13 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit if (primaryUserIdToDeleteNonAuthRecipe != null) { deleteNonAuthRecipeUser(con, appIdentifierWithStorage, primaryUserIdToDeleteNonAuthRecipe); + + // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the + // previous call to deleteAuthRecipeUser above. + deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), + true); } - // this is only done to also delete the user ID mapping in case it exists, since we do not delete in the - // previous call to deleteAuthRecipeUser above. - deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), - true); } else { for (LoginMethod lM : userToDelete.loginMethods) { io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = lM.getSupertokensUserId().equals( diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 21fe432cb..36f3e5b64 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -28,6 +28,8 @@ import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.exception.*; import io.supertokens.pluginInterface.STORAGE_TYPE; +import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; +import io.supertokens.pluginInterface.authRecipe.LoginMethod; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -389,15 +391,40 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t .addUserIdToTenant(tenantIdentifierWithStorage, userId); } + @TestOnly public static boolean removeUserIdFromTenant(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage, String userId, String externalUserId) throws FeatureNotEnabledException, TenantOrAppNotFoundException, StorageQueryException, UnknownUserIdException { + try { + return removeUserIdFromTenant(main, tenantIdentifierWithStorage, userId, externalUserId, false); + } catch (DisassociationNotAllowedException e) { + throw new IllegalStateException("should never happen"); + } + } + + public static boolean removeUserIdFromTenant(Main main, TenantIdentifierWithStorage tenantIdentifierWithStorage, + String userId, String externalUserId, boolean disallowLastTenantDisassociation) + throws FeatureNotEnabledException, TenantOrAppNotFoundException, StorageQueryException, + UnknownUserIdException, DisassociationNotAllowedException { if (Arrays.stream(FeatureFlag.getInstance(main, new AppIdentifier(null, null)).getEnabledFeatures()) .noneMatch(ee_features -> ee_features == EE_FEATURES.MULTI_TENANCY)) { throw new FeatureNotEnabledException(EE_FEATURES.MULTI_TENANCY); } + if (disallowLastTenantDisassociation) { + AuthRecipeUserInfo userInfo = AuthRecipe.getUserById(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), userId); + if (userInfo != null) { + for (LoginMethod lM : userInfo.loginMethods) { + if (lM.getSupertokensUserId().equals(userId)) { + if (lM.tenantIds.size() == 1 && lM.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + throw new DisassociationNotAllowedException(); + } + } + } + } + } + boolean finalDidExist = false; boolean didExist = AuthRecipe.deleteNonAuthRecipeUser(tenantIdentifierWithStorage, externalUserId == null ? userId : externalUserId); diff --git a/src/main/java/io/supertokens/multitenancy/exception/DisassociationNotAllowedException.java b/src/main/java/io/supertokens/multitenancy/exception/DisassociationNotAllowedException.java new file mode 100644 index 000000000..aad011c8b --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/DisassociationNotAllowedException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class DisassociationNotAllowedException extends Exception { + public DisassociationNotAllowedException() { + super("Disassociation not allowed"); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java index e97f94f64..59a630732 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java @@ -21,11 +21,13 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.DisassociationNotAllowedException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.useridmapping.UserIdType; +import io.supertokens.utils.SemVer; import io.supertokens.webserver.InputParser; import io.supertokens.webserver.WebserverAPI; import jakarta.servlet.ServletException; @@ -68,7 +70,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } boolean wasAssociated = Multitenancy.removeUserIdFromTenant(main, - getTenantIdentifierWithStorageFromRequest(req), userId, externalUserId); + getTenantIdentifierWithStorageFromRequest(req), userId, externalUserId, getVersionFromRequest(req).greaterThanOrEqualTo( + SemVer.v4_0)); JsonObject result = new JsonObject(); result.addProperty("status", "OK"); @@ -79,7 +82,10 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); result.addProperty("status", "UNKNOWN_USER_ID_ERROR"); super.sendJsonResponse(200, result, resp); - + } catch (DisassociationNotAllowedException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "DISASSOCIATION_NOT_ALLOWED_ERROR"); + super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) { throw new ServletException(e); } diff --git a/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java b/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java index 6a9627fee..7953013d6 100644 --- a/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java +++ b/src/test/java/io/supertokens/test/emailpassword/api/MultitenantAPITest.java @@ -720,7 +720,7 @@ public void testThatTenantIdIsNotAllowedForOlderCDIVersion() throws Exception { } @Test - public void testGetUserByIdForUserThatBelongsToNoTenant() throws Exception { + public void testThatDisassociationFromAllTenantsIsDisallowed() throws Exception { if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { return; } @@ -729,11 +729,16 @@ public void testGetUserByIdForUserThatBelongsToNoTenant() throws Exception { { JsonObject user = TestMultitenancyAPIHelper.epSignUp(t1, "test@example.com", "password", process.getProcess()); - TestMultitenancyAPIHelper.disassociateUserFromTenant(t1, user.get("id").getAsString(), process.getProcess()); - JsonObject userInfoFromId = TestMultitenancyAPIHelper.getEpUserById(t1, user.get("id").getAsString(), - process.getProcess()); - - assertEquals(0, userInfoFromId.get("tenantIds").getAsJsonArray().size()); + { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("userId", user.get("id").getAsString()); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + HttpRequestForTesting.getMultitenantUrl(t1, "/recipe/multitenancy/tenant/user/remove"), + requestBody, 1000, 1000, null, + SemVer.v4_0.get(), "multitenancy"); + assertEquals("DISASSOCIATION_NOT_ALLOWED_ERROR", response.get("status").getAsString()); + } } } } From 98fa30c6d2951b1fee8e061fa2ab35bc3c56d112 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 29 Aug 2023 17:17:20 +0530 Subject: [PATCH 05/12] fix: user delete --- .../java/io/supertokens/authRecipe/AuthRecipe.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index b90c37c93..68a487474 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -826,7 +826,8 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit if (!removeAllLinkedAccounts) { deleteAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForAuthRecipe, - !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId())); + !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId()) || + userToDelete.loginMethods.length == 1); if (userIdToDeleteForNonAuthRecipeForRecipeUserId != null) { deleteNonAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForNonAuthRecipeForRecipeUserId); @@ -840,7 +841,6 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), true); } - } else { for (LoginMethod lM : userToDelete.loginMethods) { io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = lM.getSupertokensUserId().equals( @@ -905,15 +905,15 @@ private static void deleteNonAuthRecipeUser(TransactionConnection con, AppIdenti private static void deleteAuthRecipeUser(TransactionConnection con, AppIdentifierWithStorage appIdentifierWithStorage, String - userId, boolean deleteUserIdMappingToo) + userId, boolean deleteFromUserIdToAppIdTableToo) throws StorageQueryException { // auth recipe deletions here only appIdentifierWithStorage.getEmailPasswordStorage() - .deleteEmailPasswordUser_Transaction(con, appIdentifierWithStorage, userId, deleteUserIdMappingToo); + .deleteEmailPasswordUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); appIdentifierWithStorage.getThirdPartyStorage() - .deleteThirdPartyUser_Transaction(con, appIdentifierWithStorage, userId, deleteUserIdMappingToo); + .deleteThirdPartyUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); appIdentifierWithStorage.getPasswordlessStorage() - .deletePasswordlessUser_Transaction(con, appIdentifierWithStorage, userId, deleteUserIdMappingToo); + .deletePasswordlessUser_Transaction(con, appIdentifierWithStorage, userId, deleteFromUserIdToAppIdTableToo); } public static boolean deleteNonAuthRecipeUser(TenantIdentifierWithStorage From 4371515e5aea17da6b1978de1a37170bb925cfb6 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Fri, 1 Sep 2023 13:16:07 +0530 Subject: [PATCH 06/12] fix: test --- src/main/java/io/supertokens/multitenancy/Multitenancy.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 50053811e..88d2b5706 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -396,7 +396,7 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t String tenantId = tenantIdentifierWithStorage.getTenantId(); AuthRecipeUserInfo userToAssociate = storage.getPrimaryUserById_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, userId); - if (userToAssociate.isPrimaryUser) { + if (userToAssociate != null && userToAssociate.isPrimaryUser) { Set emails = new HashSet<>(); Set phoneNumbers = new HashSet<>(); Set thirdParties = new HashSet<>(); @@ -442,6 +442,9 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t } } + // userToAssociate may be null if the user is not associated to any tenants, we can still try and + // associate it. This happens only in CDI 3.0 where we allow disassociation from all tenants + // This will not happen in CDI >= 4.0 because we will not allow disassociation from all tenants try { boolean result = ((MultitenancySQLStorage) storage).addUserIdToTenant_Transaction(tenantIdentifierWithStorage, con, userId); storage.commitTransaction(con); From a51130cbdfc418fea73e3995486bbbb2d0434740 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Sat, 2 Sep 2023 11:10:29 +0530 Subject: [PATCH 07/12] fix: test --- .../java/io/supertokens/inmemorydb/Start.java | 8 + .../io/supertokens/thirdparty/ThirdParty.java | 97 ++++++++---- .../test/AuthRecipesParallelTest.java | 144 ++++++++++++++++++ 3 files changed, 219 insertions(+), 30 deletions(-) create mode 100644 src/test/java/io/supertokens/test/AuthRecipesParallelTest.java diff --git a/src/main/java/io/supertokens/inmemorydb/Start.java b/src/main/java/io/supertokens/inmemorydb/Start.java index 7820d2b26..88bf47678 100644 --- a/src/main/java/io/supertokens/inmemorydb/Start.java +++ b/src/main/java/io/supertokens/inmemorydb/Start.java @@ -2786,6 +2786,14 @@ public AuthRecipeUserInfo[] listPrimaryUsersByPhoneNumber_Transaction(AppIdentif // } } + @Override + public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo(AppIdentifier appIdentifier, + String thirdPartyId, + String thirdPartyUserId) + throws StorageQueryException { + return null; // TODO + } + @Override public AuthRecipeUserInfo[] listPrimaryUsersByThirdPartyInfo_Transaction(AppIdentifier appIdentifier, TransactionConnection con, diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 366002d6e..5677bffab 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -216,12 +216,11 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan AuthRecipeSQLStorage authRecipeStorage = (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); - storage.startTransaction(con -> { + { // Try without transaction AuthRecipeUserInfo userFromDb = null; - AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo_Transaction( + AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo( appIdentifier, - con, thirdPartyId, thirdPartyUserId); for (AuthRecipeUserInfo user : usersFromDb) { if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { @@ -231,10 +230,8 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan userFromDb = user; } } - if (userFromDb == null) { - storage.commitTransaction(con); - return null; + continue; // try to create the user again } LoginMethod lM = null; @@ -250,35 +247,75 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan throw new IllegalStateException("Should never come here"); } - - if (!email.equals(lM.email)) { - // before updating the email, we must check for if another primary user has the same - // email, and if they do, then we do not allow the update. - if (userFromDb.isPrimaryUser) { - for (String tenantId : userFromDb.tenantIds) { - AuthRecipeUserInfo[] userBasedOnEmail = - authRecipeStorage.listPrimaryUsersByEmail_Transaction( - appIdentifier, con, email - ); - for (AuthRecipeUserInfo userWithSameEmail : userBasedOnEmail) { - if (!userWithSameEmail.tenantIds.contains(tenantId)) { - continue; + if (email.equals(lM.email)) { + return new SignInUpResponse(false, userFromDb); + } else { + // Email needs updating, so repeat everything in a transaction + + storage.startTransaction(con -> { + AuthRecipeUserInfo userFromDb1 = null; + + AuthRecipeUserInfo[] usersFromDb1 = authRecipeStorage.listPrimaryUsersByThirdPartyInfo_Transaction( + appIdentifier, + con, + thirdPartyId, thirdPartyUserId); + for (AuthRecipeUserInfo user : usersFromDb1) { + if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + if (userFromDb1 != null) { + throw new IllegalStateException("Should never happen"); } - if (userWithSameEmail.isPrimaryUser && - !userWithSameEmail.getSupertokensUserId().equals(userFromDb.getSupertokensUserId())) { - throw new StorageTransactionLogicException( - new EmailChangeNotAllowedException()); + userFromDb1 = user; + } + } + + if (userFromDb1 == null) { + storage.commitTransaction(con); + return null; + } + + LoginMethod lM1 = null; + for (LoginMethod loginMethod : userFromDb1.loginMethods) { + if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) && + loginMethod.thirdParty.userId.equals(thirdPartyUserId)) { + lM1 = loginMethod; + break; + } + } + + if (lM1 == null) { + throw new IllegalStateException("Should never come here"); + } + + if (!email.equals(lM1.email)) { + // before updating the email, we must check for if another primary user has the same + // email, and if they do, then we do not allow the update. + if (userFromDb1.isPrimaryUser) { + for (String tenantId : userFromDb1.tenantIds) { + AuthRecipeUserInfo[] userBasedOnEmail = + authRecipeStorage.listPrimaryUsersByEmail_Transaction( + appIdentifier, con, email + ); + for (AuthRecipeUserInfo userWithSameEmail : userBasedOnEmail) { + if (!userWithSameEmail.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSameEmail.isPrimaryUser && + !userWithSameEmail.getSupertokensUserId().equals(userFromDb1.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new EmailChangeNotAllowedException()); + } + } } } + storage.updateUserEmail_Transaction(appIdentifier, con, + thirdPartyId, thirdPartyUserId, email); } - } - storage.updateUserEmail_Transaction(appIdentifier, con, - thirdPartyId, thirdPartyUserId, email); - } - storage.commitTransaction(con); - return null; - }); + storage.commitTransaction(con); + return null; + }); + } + } AuthRecipeUserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); return new SignInUpResponse(false, user); diff --git a/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java new file mode 100644 index 000000000..fb6cc9af6 --- /dev/null +++ b/src/test/java/io/supertokens/test/AuthRecipesParallelTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.test; + +import io.supertokens.ProcessState; +import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; +import io.supertokens.emailpassword.exceptions.WrongCredentialsException; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.test.TestingProcessManager; +import io.supertokens.test.Utils; +import io.supertokens.thirdparty.ThirdParty; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class AuthRecipesParallelTest { + @Rule + public TestRule watchman = Utils.getOnFailure(); + + @AfterClass + public static void afterTesting() { + Utils.afterTesting(); + } + + @Before + public void beforeEach() { + Utils.reset(); + } + + @Test + public void timeTakenFor500SignInParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + ExecutorService ex = Executors.newFixedThreadPool(1000); + int numberOfThreads = 500; + + EmailPassword.signUp(process.getProcess(), "test@example.com", "password"); + AtomicInteger counter = new AtomicInteger(0); + AtomicInteger retryCounter = new AtomicInteger(0); + + long st = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.execute(() -> { + while(true) { + try { + EmailPassword.signIn(process.getProcess(), "test@example.com", "password"); + counter.incrementAndGet(); + break; + } catch (StorageQueryException e) { + retryCounter.incrementAndGet(); + // continue + } catch (WrongCredentialsException e) { + throw new RuntimeException(e); + } + } + }); + } + + ex.shutdown(); + + ex.awaitTermination(2, TimeUnit.MINUTES); + System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); + System.out.println("Retry counter: " + retryCounter.get()); + assertEquals(counter.get(), numberOfThreads); + assertEquals(0, retryCounter.get()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void timeTakenFor500SignInUpParallel() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + ExecutorService ex = Executors.newFixedThreadPool(1000); + int numberOfThreads = 500; + + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + AtomicInteger counter = new AtomicInteger(0); + AtomicInteger retryCounter = new AtomicInteger(0); + + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + + long st = System.currentTimeMillis(); + for (int i = 0; i < numberOfThreads; i++) { + ex.execute(() -> { + while(true) { + try { + ThirdParty.signInUp(process.getProcess(), "google", "google-user", "test@example.com"); + counter.incrementAndGet(); + break; + } catch (StorageQueryException e) { + retryCounter.incrementAndGet(); + // continue + } catch (EmailChangeNotAllowedException e) { + throw new RuntimeException(e); + } + } + }); + } + + ex.shutdown(); + + ex.awaitTermination(2, TimeUnit.MINUTES); + System.out.println("Time taken for " + numberOfThreads + " sign in parallel: " + (System.currentTimeMillis() - st) + "ms"); + System.out.println("Retry counter: " + retryCounter.get()); + assertEquals (counter.get(), numberOfThreads); + assertEquals(0, retryCounter.get()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } +} From c340a77e87da7278ea7bab883cb0c88d302364aa Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Mon, 4 Sep 2023 16:50:49 +0530 Subject: [PATCH 08/12] fix: session fix and thirdparty ev test --- .../java/io/supertokens/session/Session.java | 3 +- .../thirdparty/api/EmailVerificationTest.java | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/supertokens/session/Session.java b/src/main/java/io/supertokens/session/Session.java index c5ca0b979..63b2cb6d4 100644 --- a/src/main/java/io/supertokens/session/Session.java +++ b/src/main/java/io/supertokens/session/Session.java @@ -37,6 +37,7 @@ import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.session.noSqlStorage.SessionNoSQLStorage_1; import io.supertokens.pluginInterface.session.sqlStorage.SessionSQLStorage; +import io.supertokens.pluginInterface.sqlStorage.SQLStorage; import io.supertokens.session.accessToken.AccessToken; import io.supertokens.session.accessToken.AccessToken.AccessTokenInfo; import io.supertokens.session.info.SessionInfo; @@ -416,7 +417,7 @@ public static SessionInformationHolder getSession(AppIdentifier appIdentifier, M TenantOrAppNotFoundException e) { throw new StorageTransactionLogicException(e); } - }); + }, SQLStorage.TransactionIsolationLevel.REPEATABLE_READ); } catch (StorageTransactionLogicException e) { if (e.actualException instanceof UnauthorisedException) { throw (UnauthorisedException) e.actualException; diff --git a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java index 98bf51675..8ca4ab873 100644 --- a/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java +++ b/src/test/java/io/supertokens/test/thirdparty/api/EmailVerificationTest.java @@ -153,6 +153,75 @@ public void testEmailVerificationOnSignInUp_v4_0() throws Exception { assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + @Test + public void testEmailVerificationStateDoesNotChangeWhenFalseIsPassed() throws Exception { + String[] args = {"../"}; + + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + if (StorageLayer.getStorage(process.getProcess()).getType() != STORAGE_TYPE.SQL) { + return; + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertFalse(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", true); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + { + JsonObject emailObject = new JsonObject(); + emailObject.addProperty("id", "test@example.com"); + emailObject.addProperty("isVerified", false); + + JsonObject signUpRequestBody = new JsonObject(); + signUpRequestBody.addProperty("thirdPartyId", "google"); + signUpRequestBody.addProperty("thirdPartyUserId", "google-user"); + signUpRequestBody.add("email", emailObject); + + JsonObject response = HttpRequestForTesting.sendJsonPOSTRequest(process.getProcess(), "", + "http://localhost:3567/recipe/signinup", signUpRequestBody, 1000, 1000, null, + SemVer.v4_0.get(), "thirdparty"); + + String userId = response.get("user").getAsJsonObject().get("id").getAsString(); + assertTrue(EmailVerification.isEmailVerified(process.getProcess(), userId, "test@example.com")); + } + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + @Test public void testWithAccountLinking() throws Exception { String[] args = {"../"}; From d9410eca2613a01641f8c3271580df1d14fdde22 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Tue, 5 Sep 2023 13:07:12 +0530 Subject: [PATCH 09/12] fix: pr comments --- .../io/supertokens/authRecipe/AuthRecipe.java | 7 +- .../io/supertokens/thirdparty/ThirdParty.java | 84 +++++++++---------- .../DisassociateUserFromTenant.java | 1 + 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index 0c02f3e28..c63bb7822 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -817,6 +817,10 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit if (removeAllLinkedAccounts || userToDelete.loginMethods.length == 1) { if (userToDelete.getSupertokensUserId().equals(userIdToDeleteForAuthRecipe)) { primaryUserIdToDeleteNonAuthRecipe = userIdToDeleteForNonAuthRecipeForRecipeUserId; + if (primaryUserIdToDeleteNonAuthRecipe == null) { + deleteAuthRecipeUser(con, appIdentifierWithStorage, userToDelete.getSupertokensUserId(), + true); + } } else { // this is always type supertokens user ID cause it's from a user from the database. io.supertokens.pluginInterface.useridmapping.UserIdMapping mappingResult = @@ -840,8 +844,7 @@ private static void deleteUserHelper(TransactionConnection con, AppIdentifierWit if (!removeAllLinkedAccounts) { deleteAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForAuthRecipe, - !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId()) || - userToDelete.loginMethods.length == 1); + !userIdToDeleteForAuthRecipe.equals(userToDelete.getSupertokensUserId())); if (userIdToDeleteForNonAuthRecipeForRecipeUserId != null) { deleteNonAuthRecipeUser(con, appIdentifierWithStorage, userIdToDeleteForNonAuthRecipeForRecipeUserId); diff --git a/src/main/java/io/supertokens/thirdparty/ThirdParty.java b/src/main/java/io/supertokens/thirdparty/ThirdParty.java index 5677bffab..3100daf4a 100644 --- a/src/main/java/io/supertokens/thirdparty/ThirdParty.java +++ b/src/main/java/io/supertokens/thirdparty/ThirdParty.java @@ -211,46 +211,46 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan } // we try to get user and update their email - try { - AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier(); - AuthRecipeSQLStorage authRecipeStorage = - (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); - - { // Try without transaction - AuthRecipeUserInfo userFromDb = null; - - AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo( - appIdentifier, - thirdPartyId, thirdPartyUserId); - for (AuthRecipeUserInfo user : usersFromDb) { - if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { - if (userFromDb != null) { - throw new IllegalStateException("Should never happen"); - } - userFromDb = user; + AppIdentifier appIdentifier = tenantIdentifierWithStorage.toAppIdentifier(); + AuthRecipeSQLStorage authRecipeStorage = + (AuthRecipeSQLStorage) tenantIdentifierWithStorage.getAuthRecipeStorage(); + + { // Try without transaction, because in most cases we might not need to update the email + AuthRecipeUserInfo userFromDb = null; + + AuthRecipeUserInfo[] usersFromDb = authRecipeStorage.listPrimaryUsersByThirdPartyInfo( + appIdentifier, + thirdPartyId, thirdPartyUserId); + for (AuthRecipeUserInfo user : usersFromDb) { + if (user.tenantIds.contains(tenantIdentifierWithStorage.getTenantId())) { + if (userFromDb != null) { + throw new IllegalStateException("Should never happen"); } + userFromDb = user; } - if (userFromDb == null) { - continue; // try to create the user again - } + } + if (userFromDb == null) { + continue; // try to create the user again + } - LoginMethod lM = null; - for (LoginMethod loginMethod : userFromDb.loginMethods) { - if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) && - loginMethod.thirdParty.userId.equals(thirdPartyUserId)) { - lM = loginMethod; - break; - } + LoginMethod lM = null; + for (LoginMethod loginMethod : userFromDb.loginMethods) { + if (loginMethod.thirdParty != null && loginMethod.thirdParty.id.equals(thirdPartyId) && + loginMethod.thirdParty.userId.equals(thirdPartyUserId)) { + lM = loginMethod; + break; } + } - if (lM == null) { - throw new IllegalStateException("Should never come here"); - } + if (lM == null) { + throw new IllegalStateException("Should never come here"); + } - if (email.equals(lM.email)) { - return new SignInUpResponse(false, userFromDb); - } else { - // Email needs updating, so repeat everything in a transaction + if (email.equals(lM.email)) { + return new SignInUpResponse(false, userFromDb); + } else { + // Email needs updating, so repeat everything in a transaction + try { storage.startTransaction(con -> { AuthRecipeUserInfo userFromDb1 = null; @@ -314,19 +314,17 @@ private static SignInUpResponse signInUpHelper(TenantIdentifierWithStorage tenan storage.commitTransaction(con); return null; }); + } catch (StorageTransactionLogicException e) { + if (e.actualException instanceof EmailChangeNotAllowedException) { + throw (EmailChangeNotAllowedException) e.actualException; + } + throw new StorageQueryException(e); } } - - AuthRecipeUserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); - return new SignInUpResponse(false, user); - } catch (StorageTransactionLogicException e) { - if (e.actualException instanceof EmailChangeNotAllowedException) { - throw (EmailChangeNotAllowedException) e.actualException; - } - throw new StorageQueryException(e); } - // retry.. + AuthRecipeUserInfo user = getUser(tenantIdentifierWithStorage, thirdPartyId, thirdPartyUserId); + return new SignInUpResponse(false, user); } } diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java index 59a630732..023f91342 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/DisassociateUserFromTenant.java @@ -85,6 +85,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } catch (DisassociationNotAllowedException e) { JsonObject result = new JsonObject(); result.addProperty("status", "DISASSOCIATION_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "The user belongs to only one tenant and cannot be disassociated from that"); super.sendJsonResponse(200, result, resp); } catch (StorageQueryException | TenantOrAppNotFoundException | FeatureNotEnabledException e) { throw new ServletException(e); From 5134184935154e0d3719aa88d8f12270bf09b563 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 6 Sep 2023 13:30:31 +0530 Subject: [PATCH 10/12] fix: pr comments --- .../multitenancy/Multitenancy.java | 44 +- ...ryUserWithEmailAlreadyExistsException.java | 23 + ...WithPhoneNumberAlreadyExistsException.java | 23 + ...hThirdPartyInfoAlreadyExistsException.java | 23 + .../AssociateUserToTenantAPI.java | 10 + .../test/accountlinking/MultitenantTest.java | 643 +++++++++++------- 6 files changed, 515 insertions(+), 251 deletions(-) create mode 100644 src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java create mode 100644 src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java create mode 100644 src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java diff --git a/src/main/java/io/supertokens/multitenancy/Multitenancy.java b/src/main/java/io/supertokens/multitenancy/Multitenancy.java index 88d2b5706..1aaa2a61e 100644 --- a/src/main/java/io/supertokens/multitenancy/Multitenancy.java +++ b/src/main/java/io/supertokens/multitenancy/Multitenancy.java @@ -384,7 +384,9 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t String userId) throws TenantOrAppNotFoundException, UnknownUserIdException, StorageQueryException, FeatureNotEnabledException, DuplicateEmailException, DuplicatePhoneNumberException, - DuplicateThirdPartyUserException { + DuplicateThirdPartyUserException, AnotherPrimaryUserWithPhoneNumberAlreadyExistsException, + AnotherPrimaryUserWithEmailAlreadyExistsException, + AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException { if (Arrays.stream(FeatureFlag.getInstance(main, new AppIdentifier(null, null)).getEnabledFeatures()) .noneMatch(ee_features -> ee_features == EE_FEATURES.MULTI_TENANCY)) { throw new FeatureNotEnabledException(EE_FEATURES.MULTI_TENANCY); @@ -418,7 +420,16 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t AuthRecipeUserInfo[] users = storage.listPrimaryUsersByEmail_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, email); for (AuthRecipeUserInfo user : users) { if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { - throw new StorageTransactionLogicException(new DuplicateEmailException()); + for (LoginMethod lm1 : user.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && email.equals(lm1.email) && lm1.email.equals(lm2.email)) { + throw new StorageTransactionLogicException(new DuplicateEmailException()); + } + } + } + } + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithEmailAlreadyExistsException(user.getSupertokensUserId())); } } } @@ -427,7 +438,16 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t AuthRecipeUserInfo[] users = storage.listPrimaryUsersByPhoneNumber_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, phoneNumber); for (AuthRecipeUserInfo user : users) { if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { - throw new StorageTransactionLogicException(new DuplicatePhoneNumberException()); + for (LoginMethod lm1 : user.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && phoneNumber.equals(lm1.phoneNumber) && lm1.phoneNumber.equals(lm2.phoneNumber)) { + throw new StorageTransactionLogicException(new DuplicatePhoneNumberException()); + } + } + } + } + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(user.getSupertokensUserId())); } } } @@ -436,7 +456,17 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t AuthRecipeUserInfo[] users = storage.listPrimaryUsersByThirdPartyInfo_Transaction(tenantIdentifierWithStorage.toAppIdentifier(), con, tp.id, tp.userId); for (AuthRecipeUserInfo user : users) { if (user.tenantIds.contains(tenantId) && !user.getSupertokensUserId().equals(userId)) { - throw new StorageTransactionLogicException(new DuplicateThirdPartyUserException()); + for (LoginMethod lm1 : user.loginMethods) { + if (lm1.tenantIds.contains(tenantId)) { + for (LoginMethod lm2 : userToAssociate.loginMethods) { + if (lm1.recipeId.equals(lm2.recipeId) && tp.equals(lm1.thirdParty) && lm1.thirdParty.equals(lm2.thirdParty)) { + throw new StorageTransactionLogicException(new DuplicateThirdPartyUserException()); + } + } + } + } + + throw new StorageTransactionLogicException(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(user.getSupertokensUserId())); } } } @@ -465,6 +495,12 @@ public static boolean addUserIdToTenant(Main main, TenantIdentifierWithStorage t throw (TenantOrAppNotFoundException) e.actualException; } else if (e.actualException instanceof UnknownUserIdException) { throw (UnknownUserIdException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) { + throw (AnotherPrimaryUserWithPhoneNumberAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithEmailAlreadyExistsException) { + throw (AnotherPrimaryUserWithEmailAlreadyExistsException) e.actualException; + } else if (e.actualException instanceof AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) { + throw (AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException) e.actualException; } throw new StorageQueryException(e.actualException); } diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java new file mode 100644 index 000000000..c95bfdcc3 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithEmailAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithEmailAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithEmailAlreadyExistsException(String primaryUserId) { + super("Another primary user with email already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java new file mode 100644 index 000000000..e012f9349 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithPhoneNumberAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithPhoneNumberAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithPhoneNumberAlreadyExistsException(String primaryUserId) { + super("Another primary user with phone number already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java new file mode 100644 index 000000000..d5f413cf5 --- /dev/null +++ b/src/main/java/io/supertokens/multitenancy/exception/AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.multitenancy.exception; + +public class AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException extends Exception { + public AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException(String primaryUserId) { + super("Another primary user with third party info already exists: " + primaryUserId); + } +} diff --git a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java index d53c2e7e8..3eb502dc5 100644 --- a/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java +++ b/src/main/java/io/supertokens/webserver/api/multitenancy/AssociateUserToTenantAPI.java @@ -21,6 +21,9 @@ import io.supertokens.Main; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithPhoneNumberAlreadyExistsException; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.emailpassword.exceptions.UnknownUserIdException; @@ -99,6 +102,13 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I JsonObject result = new JsonObject(); result.addProperty("status", "THIRD_PARTY_USER_ALREADY_EXISTS_ERROR"); super.sendJsonResponse(200, result, resp); + + } catch (AnotherPrimaryUserWithEmailAlreadyExistsException | AnotherPrimaryUserWithPhoneNumberAlreadyExistsException | + AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "ASSOCIATION_NOT_ALLOWED_ERROR"); + result.addProperty("reason", e.getMessage()); + super.sendJsonResponse(200, result, resp); } } } diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java index f9cdc8e12..09e104436 100644 --- a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -22,11 +22,14 @@ import io.supertokens.authRecipe.AuthRecipe; import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; import io.supertokens.emailpassword.EmailPassword; +import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException; +import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException; import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; import io.supertokens.passwordless.Passwordless; @@ -48,10 +51,11 @@ import org.junit.rules.TestRule; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.function.Function; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; public class MultitenantTest { @Rule @@ -67,7 +71,7 @@ public void beforeEach() { Utils.reset(); } - TenantIdentifier t1, t2, t3; + TenantIdentifier t1, t2, t3, t4; private void createTenants(Main main) throws StorageQueryException, TenantOrAppNotFoundException, InvalidProviderConfigException, @@ -136,292 +140,437 @@ private void createTenants(Main main) ); } + { // tenant 4 + JsonObject config = new JsonObject(); + TenantIdentifier tenantIdentifier = new TenantIdentifier(null, "a1", "t3"); + + StorageLayer.getStorage(new TenantIdentifier(null, null, null), main) + .modifyConfigToAddANewUserPoolForTesting(config, 1); + + Multitenancy.addNewOrUpdateAppOrTenant( + main, + new TenantIdentifier(null, "a1", null), + new TenantConfig( + tenantIdentifier, + new EmailPasswordConfig(true), + new ThirdPartyConfig(true, null), + new PasswordlessConfig(true), + config + ) + ); + } + } + + @Test + public void testVariousCases() throws Exception { t1 = new TenantIdentifier(null, "a1", null); t2 = new TenantIdentifier(null, "a1", "t1"); t3 = new TenantIdentifier(null, "a1", "t2"); + t4 = new TenantIdentifier(null, "a1", "t3"); + + TestCase[] testCases = new TestCase[]{ + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t2, "test2@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new UpdatePlessUserEmail(t1, 0, "test2@example.com"), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithEmail(t1, "test1@example.com"), + new CreatePlessUserWithEmail(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2).expect(new DuplicateEmailException()), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t1, 2), + new AssociateUserToTenant(t2, 2), // Allowed + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithEmailAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid", "test1@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new CreateEmailPasswordUser(t3, "test1@example.com"), + new AssociateUserToTenant(t2, 2), + new MakePrimaryUser(t3, 2).expect(new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException("", "")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new CreateThirdPartyUser(t1, "google", "googleid1", "test2@example.com"), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + }; + + int i = 0; + for (TestCase testCase : testCases) { + String[] args = {"../"}; + TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); + FeatureFlagTestContent.getInstance(process.getProcess()) + .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ + EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); + process.startProcess(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + createTenants(process.getProcess()); + + System.out.println("Executing test case : " + i); + testCase.doTest(process.getProcess()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + i++; + } } - @Test - public void testWithEmailPasswordUsers1() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); - AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password"); - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - try { - // Credentials does not exist in t2 - EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); - fail(); - } catch (WrongCredentialsException e) { - // ignore + private static class TestCase { + TestCaseStep[] steps; + public static List users; + + public static void resetUsers() { + users = new ArrayList<>(); } - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user1.getSupertokensUserId()); + public static void addUser(AuthRecipeUserInfo user) { + users.add(user); + } - // Sign in should now pass - EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); + public TestCase(TestCaseStep[] steps) { + this.steps = steps; + } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); - } + public void doTest(Main main) throws Exception { + TestCase.resetUsers(); - @Test - public void testWithEmailPasswordUsers2() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); - AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password"); - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - try { - // Credentials does not exist in t2 - EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); - fail(); - } catch (WrongCredentialsException e) { - // ignore + for (TestCaseStep step : steps) { + step.doStep(main); + } } + } - // same email is allowed to sign up - AuthRecipeUserInfo user3 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + private static class TestCaseStep { + Exception e; - // Sign in should pass - EmailPassword.signIn(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + public TestCaseStep expect(Exception e) { + this.e = e; + return this; + } - try { - // user 3 cannot become a primary user - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - fail(); - } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { - // Ignore + public void doStep(Main main) throws Exception { + if (e == null) { + this.execute(main); + } else { + try { + this.execute(main); + fail(); + } catch (Exception e) { + assertEquals(this.e.getClass(), e.getClass()); + } + } } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + public void execute(Main main) throws Exception { + } } - @Test - public void testWithEmailPasswordUsersAndPasswordlessUser1() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); - Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t2WithStorage, process.getProcess(), - "test2@example.com", null, null, null); - AuthRecipeUserInfo user2 = Passwordless.consumeCode(t2WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user; - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - try { - // Credentials does not exist in t2 - EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); - fail(); - } catch (WrongCredentialsException e) { - // ignore + private static class CreateEmailPasswordUser extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String email; + + public CreateEmailPasswordUser(TenantIdentifier tenantIdentifier, String email) { + this.tenantIdentifier = tenantIdentifier; + this.email = email; } - // same email is allowed to sign up - AuthRecipeUserInfo user3 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipeUserInfo user = EmailPassword.signUp(tenantIdentifierWithStorage, main, email, "password"); + TestCase.addUser(user); + } + } - // Sign in should pass - EmailPassword.signIn(t1WithStorage, process.getProcess(), "test2@example.com", "password2"); + private static class CreatePlessUserWithEmail extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String email; - try { - // user 3 cannot become a primary user - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - fail(); - } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { - // Ignore + public CreatePlessUserWithEmail(TenantIdentifier tenantIdentifier, String email) { + this.tenantIdentifier = tenantIdentifier; + this.email = email; } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main, + email, null, null, null); + AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user; + TestCase.addUser(user); + } } - @Test - public void testWithEmailPasswordUsersAndPasswordlessUser2() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); - Passwordless.CreateCodeResponse user2Code = Passwordless.createCode(t2WithStorage, process.getProcess(), - "test2@example.com", null, null, null); - AuthRecipeUserInfo user2 = Passwordless.consumeCode(t2WithStorage, process.getProcess(), user2Code.deviceId, user2Code.deviceIdHash, user2Code.userInputCode, null).user; - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - // same email is allowed to sign in up - Passwordless.CreateCodeResponse user3code = Passwordless.createCode(t1WithStorage, process.getProcess(), - "test2@example.com", null, null, null); - - AuthRecipeUserInfo user3 = Passwordless.consumeCode(t1WithStorage, process.getProcess(), user3code.deviceId, user3code.deviceIdHash, user3code.userInputCode, null).user; - - try { - // user 3 cannot become a primary user - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - fail(); - } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { - // Ignore + private static class CreateThirdPartyUser extends TestCaseStep { + TenantIdentifier tenantIdentifier; + String thirdPartyId; + String thirdPartyUserId; + String email; + + public CreateThirdPartyUser(TenantIdentifier tenantIdentifier, String thirdPartyId, String thirdPartyUserId, String email) { + this.tenantIdentifier = tenantIdentifier; + this.thirdPartyId = thirdPartyId; + this.thirdPartyUserId = thirdPartyUserId; + this.email = email; } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipeUserInfo user = ThirdParty.signInUp(tenantIdentifierWithStorage, main, thirdPartyId, thirdPartyUserId, email).user; + TestCase.addUser(user); + } } - @Test - public void testWithEmailPasswordUserAndThirdPartyUser() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password"); - AuthRecipeUserInfo user2 = ThirdParty.signInUp(t2WithStorage, process.getProcess(), "google", "google-user", "test2@example.com").user; - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - try { - // Credentials does not exist in t2 - EmailPassword.signIn(t2WithStorage, process.getProcess(), "test1@example.com", "password"); - fail(); - } catch (WrongCredentialsException e) { - // ignore + private static class MakePrimaryUser extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public MakePrimaryUser(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipe.createPrimaryUser(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId()); } + } - // same email is allowed to sign up - AuthRecipeUserInfo user3 = ThirdParty.signInUp(t1WithStorage, process.getProcess(), "google", "google-user", "test2@example.com").user; + private static class LinkAccounts extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int primaryUserIndex; + int recipeUserIndex; - try { - // user 3 cannot become a primary user - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - fail(); - } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { - // Ignore + public LinkAccounts(TenantIdentifier tenantIdentifier, int primaryUserIndex, int recipeUserIndex) { + this.tenantIdentifier = tenantIdentifier; + this.primaryUserIndex = primaryUserIndex; + this.recipeUserIndex = recipeUserIndex; } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + AuthRecipe.linkAccounts(main, tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(recipeUserIndex).getSupertokensUserId(), TestCase.users.get(primaryUserIndex).getSupertokensUserId()); + } } - @Test - public void testTenantAssociationWithEPUsers1() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - TenantIdentifierWithStorage t3WithStorage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password1"); - AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password2"); - AuthRecipeUserInfo user3 = EmailPassword.signUp(t3WithStorage, process.getProcess(), "test1@example.com", "password3"); - - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user3.getSupertokensUserId()); - try { - AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - fail(); - } catch (AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException e) { - // ignore + private static class AssociateUserToTenant extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + + public AssociateUserToTenant(TenantIdentifier tenantIdentifier, int userIndex) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Multitenancy.addUserIdToTenant(main, tenantIdentifierWithStorage, TestCase.users.get(userIndex).getSupertokensUserId()); + } } - @Test - public void testTenantAssociationWithEPUsers2() throws Exception { - String[] args = {"../"}; - TestingProcessManager.TestingProcess process = TestingProcessManager.start(args, false); - FeatureFlagTestContent.getInstance(process.getProcess()) - .setKeyValue(FeatureFlagTestContent.ENABLED_FEATURES, new EE_FEATURES[]{ - EE_FEATURES.ACCOUNT_LINKING, EE_FEATURES.MULTI_TENANCY}); - process.startProcess(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); - - createTenants(process.getProcess()); - - TenantIdentifierWithStorage t1WithStorage = t1.withStorage(StorageLayer.getStorage(t1, process.getProcess())); - TenantIdentifierWithStorage t2WithStorage = t2.withStorage(StorageLayer.getStorage(t2, process.getProcess())); - TenantIdentifierWithStorage t3WithStorage = t3.withStorage(StorageLayer.getStorage(t3, process.getProcess())); - - AuthRecipeUserInfo user1 = EmailPassword.signUp(t1WithStorage, process.getProcess(), "test1@example.com", "password1"); - AuthRecipeUserInfo user2 = EmailPassword.signUp(t2WithStorage, process.getProcess(), "test2@example.com", "password2"); - AuthRecipeUserInfo user3 = EmailPassword.signUp(t3WithStorage, process.getProcess(), "test1@example.com", "password3"); - - AuthRecipe.createPrimaryUser(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user1.getSupertokensUserId()); - AuthRecipe.linkAccounts(process.getProcess(), t1WithStorage.toAppIdentifierWithStorage(), user2.getSupertokensUserId(), user1.getSupertokensUserId()); - - AuthRecipe.createPrimaryUser(process.getProcess(), t2WithStorage.toAppIdentifierWithStorage(), user3.getSupertokensUserId()); - try { - Multitenancy.addUserIdToTenant(process.getProcess(), t2WithStorage, user3.getSupertokensUserId()); - fail(); - } catch (DuplicateEmailException e) { - // ignore + private static class UpdatePlessUserEmail extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String email; + + public UpdatePlessUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.email = email; } - process.kill(); - assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), new Passwordless.FieldUpdate(email), null); + } } } From ec286b20b9779feb7df672aeed91cfa5add65e49 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 6 Sep 2023 14:49:15 +0530 Subject: [PATCH 11/12] fix: tests --- .../io/supertokens/authRecipe/AuthRecipe.java | 3 + .../test/accountlinking/MultitenantTest.java | 58 +++++++++++++++++-- 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java index c63bb7822..5633ebf1d 100644 --- a/src/main/java/io/supertokens/authRecipe/AuthRecipe.java +++ b/src/main/java/io/supertokens/authRecipe/AuthRecipe.java @@ -494,6 +494,9 @@ private static CreatePrimaryUserResult canCreatePrimaryUserHelper(TransactionCon .listPrimaryUsersByPhoneNumber_Transaction(appIdentifierWithStorage, con, loginMethod.phoneNumber); for (AuthRecipeUserInfo user : usersWithSamePhoneNumber) { + if (!user.tenantIds.contains(tenantId)) { + continue; + } if (user.isPrimaryUser) { throw new AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException(user.getSupertokensUserId(), "This user's phone number is already associated with another user" + diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java index 09e104436..c67cc03bc 100644 --- a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -28,10 +28,7 @@ import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; -import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithEmailAlreadyExistsException; -import io.supertokens.multitenancy.exception.AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException; -import io.supertokens.multitenancy.exception.BadPermissionException; -import io.supertokens.multitenancy.exception.CannotModifyBaseConfigException; +import io.supertokens.multitenancy.exception.*; import io.supertokens.passwordless.Passwordless; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; @@ -39,6 +36,8 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.*; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; +import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; +import io.supertokens.pluginInterface.thirdparty.exception.DuplicateThirdPartyUserException; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; @@ -376,6 +375,38 @@ public void testVariousCases() throws Exception { new MakePrimaryUser(t2, 1), new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()), }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new CreatePlessUserWithPhone(t3, "+1000001"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t1, 2).expect(new DuplicatePhoneNumberException()), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithPhoneNumberAlreadyExistsException("")), + }), + + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateThirdPartyUser(t3, "google", "googleid1", "test3@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t3, 2), + new AssociateUserToTenant(t1, 2).expect(new DuplicateThirdPartyUserException()), + new AssociateUserToTenant(t2, 2).expect(new AnotherPrimaryUserWithThirdPartyInfoAlreadyExistsException("")), + }), + new TestCase(new TestCaseStep[]{ + new CreateThirdPartyUser(t1, "google", "googleid1", "test1@example.com"), + new CreateThirdPartyUser(t2, "google", "googleid2", "test2@example.com"), + new CreateThirdPartyUser(t1, "google", "googleid3", "test3@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 1), + new MakePrimaryUser(t1, 2), + new CreateThirdPartyUser(t1, "google", "googleid1", "test3@example.com").expect(new EmailChangeNotAllowedException()), + new CreateThirdPartyUser(t1, "google", "googleid3", "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), }; int i = 0; @@ -485,6 +516,25 @@ public void execute(Main main) throws Exception { } } + private static class CreatePlessUserWithPhone extends TestCaseStep { + private final TenantIdentifier tenantIdentifier; + private final String phoneNumber; + + public CreatePlessUserWithPhone(TenantIdentifier tenantIdentifier, String phoneNumber) { + this.tenantIdentifier = tenantIdentifier; + this.phoneNumber = phoneNumber; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.CreateCodeResponse code = Passwordless.createCode(tenantIdentifierWithStorage, main, + null, phoneNumber, null, null); + AuthRecipeUserInfo user = Passwordless.consumeCode(tenantIdentifierWithStorage, main, code.deviceId, code.deviceIdHash, code.userInputCode, null).user; + TestCase.addUser(user); + } + } + private static class CreateThirdPartyUser extends TestCaseStep { TenantIdentifier tenantIdentifier; String thirdPartyId; From 74a9ab12beee1b18f5648438aa7a318addef09e9 Mon Sep 17 00:00:00 2001 From: Sattvik Chakravarthy Date: Wed, 6 Sep 2023 15:36:02 +0530 Subject: [PATCH 12/12] fix: phone number change related --- .../passwordless/Passwordless.java | 29 ++++++- .../PhoneNumberChangeNotAllowedException.java | 20 +++++ .../webserver/api/passwordless/UserAPI.java | 7 +- .../test/accountlinking/MultitenantTest.java | 79 ++++++++++++++++++- 4 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java diff --git a/src/main/java/io/supertokens/passwordless/Passwordless.java b/src/main/java/io/supertokens/passwordless/Passwordless.java index d0b06dc0b..930e61493 100644 --- a/src/main/java/io/supertokens/passwordless/Passwordless.java +++ b/src/main/java/io/supertokens/passwordless/Passwordless.java @@ -35,7 +35,6 @@ import io.supertokens.pluginInterface.exceptions.StorageTransactionLogicException; import io.supertokens.pluginInterface.multitenancy.AppIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.TenantConfig; -import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import io.supertokens.pluginInterface.multitenancy.TenantIdentifierWithStorage; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.PasswordlessCode; @@ -670,7 +669,8 @@ public static AuthRecipeUserInfo getUserByEmail(TenantIdentifierWithStorage tena public static void updateUser(Main main, String userId, FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException, - DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException { + DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException, + PhoneNumberChangeNotAllowedException { Storage storage = StorageLayer.getStorage(main); updateUser(new AppIdentifierWithStorage(null, null, storage), userId, emailUpdate, phoneNumberUpdate); @@ -679,7 +679,8 @@ public static void updateUser(Main main, String userId, public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, String recipeUserId, FieldUpdate emailUpdate, FieldUpdate phoneNumberUpdate) throws StorageQueryException, UnknownUserIdException, DuplicateEmailException, - DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException { + DuplicatePhoneNumberException, UserWithoutContactInfoException, EmailChangeNotAllowedException, + PhoneNumberChangeNotAllowedException { PasswordlessSQLStorage storage = appIdentifierWithStorage.getPasswordlessStorage(); // We do not lock the user here, because we decided that even if the device cleanup used outdated information @@ -742,6 +743,24 @@ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, } } if (phoneNumberUpdate != null && !Objects.equals(phoneNumberUpdate.newValue, lM.phoneNumber)) { + if (user.isPrimaryUser) { + for (String tenantId : user.tenantIds) { + AuthRecipeUserInfo[] existingUsersWithNewPhoneNumber = + authRecipeSQLStorage.listPrimaryUsersByPhoneNumber_Transaction( + appIdentifierWithStorage, con, + phoneNumberUpdate.newValue); + + for (AuthRecipeUserInfo userWithSamePhoneNumber : existingUsersWithNewPhoneNumber) { + if (!userWithSamePhoneNumber.tenantIds.contains(tenantId)) { + continue; + } + if (userWithSamePhoneNumber.isPrimaryUser && !userWithSamePhoneNumber.getSupertokensUserId().equals(user.getSupertokensUserId())) { + throw new StorageTransactionLogicException( + new PhoneNumberChangeNotAllowedException()); + } + } + } + } try { storage.updateUserPhoneNumber_Transaction(appIdentifierWithStorage, con, recipeUserId, phoneNumberUpdate.newValue); @@ -776,6 +795,10 @@ public static void updateUser(AppIdentifierWithStorage appIdentifierWithStorage, if (e.actualException instanceof EmailChangeNotAllowedException) { throw (EmailChangeNotAllowedException) e.actualException; } + + if (e.actualException instanceof PhoneNumberChangeNotAllowedException) { + throw (PhoneNumberChangeNotAllowedException) e.actualException; + } } } diff --git a/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java new file mode 100644 index 000000000..d0117d1cc --- /dev/null +++ b/src/main/java/io/supertokens/passwordless/exceptions/PhoneNumberChangeNotAllowedException.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.passwordless.exceptions; + +public class PhoneNumberChangeNotAllowedException extends Exception { +} diff --git a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java index 76d352bb2..253a3f81a 100644 --- a/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java +++ b/src/main/java/io/supertokens/webserver/api/passwordless/UserAPI.java @@ -22,6 +22,7 @@ import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; import io.supertokens.passwordless.Passwordless; import io.supertokens.passwordless.Passwordless.FieldUpdate; +import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException; import io.supertokens.passwordless.exceptions.UserWithoutContactInfoException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; @@ -30,7 +31,6 @@ import io.supertokens.pluginInterface.exceptions.StorageQueryException; import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; import io.supertokens.pluginInterface.passwordless.exception.DuplicatePhoneNumberException; -import io.supertokens.pluginInterface.useridmapping.UserIdMapping; import io.supertokens.useridmapping.UserIdType; import io.supertokens.utils.SemVer; import io.supertokens.utils.Utils; @@ -187,6 +187,11 @@ protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IO result.addProperty("status", "EMAIL_CHANGE_NOT_ALLOWED_ERROR"); result.addProperty("reason", "New email is associated with another primary user ID"); super.sendJsonResponse(200, result, resp); + } catch (PhoneNumberChangeNotAllowedException e) { + JsonObject result = new JsonObject(); + result.addProperty("status", "PHONE_NUMBER_CHANGE_NOT_ALLOWED_ERROR"); + result.addProperty("reason", "New phone number is associated with another primary user ID"); + super.sendJsonResponse(200, result, resp); } } } diff --git a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java index c67cc03bc..17ff75764 100644 --- a/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java +++ b/src/test/java/io/supertokens/test/accountlinking/MultitenantTest.java @@ -23,13 +23,13 @@ import io.supertokens.authRecipe.exception.AccountInfoAlreadyAssociatedWithAnotherPrimaryUserIdException; import io.supertokens.emailpassword.EmailPassword; import io.supertokens.emailpassword.exceptions.EmailChangeNotAllowedException; -import io.supertokens.emailpassword.exceptions.WrongCredentialsException; import io.supertokens.featureflag.EE_FEATURES; import io.supertokens.featureflag.FeatureFlagTestContent; import io.supertokens.featureflag.exceptions.FeatureNotEnabledException; import io.supertokens.multitenancy.Multitenancy; import io.supertokens.multitenancy.exception.*; import io.supertokens.passwordless.Passwordless; +import io.supertokens.passwordless.exceptions.PhoneNumberChangeNotAllowedException; import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo; import io.supertokens.pluginInterface.emailpassword.exceptions.DuplicateEmailException; import io.supertokens.pluginInterface.exceptions.InvalidConfigException; @@ -52,7 +52,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.function.Function; import static org.junit.Assert.*; @@ -287,6 +286,46 @@ public void testVariousCases() throws Exception { new UpdatePlessUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()), }), + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t1, "+1000003"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserPhone(t1, 0, "+1000003").expect(new PhoneNumberChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreatePlessUserWithPhone(t1, "+1000001"), + new CreatePlessUserWithPhone(t1, "+1000003"), + new CreatePlessUserWithPhone(t2, "+1000002"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdatePlessUserPhone(t1, 1, "+1000001").expect(new PhoneNumberChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdateEmailPasswordUserEmail(t1, 0, "test3@example.com").expect(new EmailChangeNotAllowedException()), + }), + + new TestCase(new TestCaseStep[]{ + new CreateEmailPasswordUser(t1, "test1@example.com"), + new CreateEmailPasswordUser(t1, "test3@example.com"), + new CreateEmailPasswordUser(t2, "test2@example.com"), + new MakePrimaryUser(t1, 0), + new LinkAccounts(t1, 0, 2), + new MakePrimaryUser(t2, 1), + new UpdateEmailPasswordUserEmail(t1, 1, "test1@example.com").expect(new EmailChangeNotAllowedException()), + }), + new TestCase(new TestCaseStep[]{ new CreateEmailPasswordUser(t1, "test1@example.com"), new CreateThirdPartyUser(t2, "google", "googleid", "test2@example.com"), @@ -606,6 +645,24 @@ public void execute(Main main) throws Exception { } } + private static class UpdateEmailPasswordUserEmail extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String email; + + public UpdateEmailPasswordUserEmail(TenantIdentifier tenantIdentifier, int userIndex, String email) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.email = email; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + EmailPassword.updateUsersEmailOrPassword(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), main, TestCase.users.get(userIndex).getSupertokensUserId(), email, null); + } + } + private static class UpdatePlessUserEmail extends TestCaseStep { TenantIdentifier tenantIdentifier; int userIndex; @@ -623,4 +680,22 @@ public void execute(Main main) throws Exception { Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), new Passwordless.FieldUpdate(email), null); } } + + private static class UpdatePlessUserPhone extends TestCaseStep { + TenantIdentifier tenantIdentifier; + int userIndex; + String phoneNumber; + + public UpdatePlessUserPhone(TenantIdentifier tenantIdentifier, int userIndex, String phoneNumber) { + this.tenantIdentifier = tenantIdentifier; + this.userIndex = userIndex; + this.phoneNumber = phoneNumber; + } + + @Override + public void execute(Main main) throws Exception { + TenantIdentifierWithStorage tenantIdentifierWithStorage = tenantIdentifier.withStorage(StorageLayer.getStorage(tenantIdentifier, main)); + Passwordless.updateUser(tenantIdentifierWithStorage.toAppIdentifierWithStorage(), TestCase.users.get(userIndex).getSupertokensUserId(), null, new Passwordless.FieldUpdate(phoneNumber)); + } + } }