fix(registration): check input and store emails in lower case

This commit is contained in:
lobo committed 2026-09-29 13:27:31 +02:00
1 parent ea4ae089ff
commit 68919eaab4
15 files changed
+173 -11

No files matched your search

@@ -13,6 +13,7 @@ import auth_service.shared.security.SessionProperties;
import jakarta.servlet.http.Cookie;
import auth_service.shared.test.AbstractWebMvcTest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
import org.springframework.boot.test.json.JacksonTester;
@@ -26,6 +27,7 @@ import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@@ -183,4 +185,18 @@ class LoginControllerTest extends AbstractWebMvcTest {
verify(refreshTokenService).revoke("the-refresh");
}
@Test
void login_Email_IsTrimmedAndLowerCase() throws Exception {
when(loginService.login(any(User.class), any(), any())).thenReturn(new Session("mock-access", "mock-refresh"));
mockMvc.perform(post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(loginRequestJson.write(new LoginRequest(" Bob@Example.COM ", "password")).getJson()))
.andExpect(status().isOk());
ArgumentCaptor<User> user = ArgumentCaptor.forClass(User.class);
verify(loginService).login(user.capture(), any(), any());
assertEquals("bob@example.com", user.getValue().getEmail());
}
}
@@ -57,4 +57,13 @@ class MinimalUserRepositoryTest extends AbstractPersistenceTest {
var updated = entityManager.find(MinimalUserEntity.class, entity.getId());
assertNotNull(updated.getLastLoginDate());
}
// An access token from before V19 still carries the email as it was typed.
@Test
void findByEmail_IgnoresCase() {
entityManager.persist(MinimalUserEntity.builder().email("case@example.com").password("password").enabled(true).build());
entityManager.flush();
assertTrue(minimalUserRepository.findByEmail("Case@Example.com").isPresent());
}
}
@@ -9,6 +9,8 @@ import auth_service.modules.registration.model.Registration;
import auth_service.modules.registration.service.RegistrationService;
import auth_service.shared.test.AbstractWebMvcTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJsonTesters;
@@ -187,4 +189,53 @@ class RegistrationControllerTest extends AbstractWebMvcTest {
.andExpect(header().string("Retry-After", "900"))
.andExpect(jsonPath("$.code").value("TOO_MANY_ATTEMPTS"));
}
@ParameterizedTest
@CsvSource(delimiter = '|', value = {
" |SuperSecret123!|code",
"no-at-sign|SuperSecret123!|code",
"test@example.com| |code",
"test@example.com|short|code",
"test@example.com|SuperSecret123!| ",
})
void register_InvalidInput_Is400WithoutRegistering(String email, String password, String inviteCode) throws Exception {
var request = RegistrationRequest.builder().email(email).password(password).inviteCode(inviteCode).build();
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isBadRequest());
verifyNoInteractions(registrationService);
}
// BCrypt uses only 72 bytes: a longer password would mostly be ignored, or refused with a 500.
@Test
void register_PasswordOver72Bytes_Is400() throws Exception {
var request = RegistrationRequest.builder().email("test@example.com").password("é".repeat(37))
.inviteCode("code").build();
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isBadRequest());
verifyNoInteractions(registrationService);
}
@Test
void register_Email_IsTrimmedAndLowerCase() throws Exception {
var request = RegistrationRequest.builder().email(" Bob@Example.COM ").password("SuperSecret123!")
.inviteCode("code").build();
when(registrationService.register(any(Registration.class), any())).thenReturn(1L);
mockMvc.perform(post("/api/registration/new")
.contentType(MediaType.APPLICATION_JSON)
.content(registrationRequestJson.write(request).getJson()))
.andExpect(status().isOk());
ArgumentCaptor<Registration> registration = ArgumentCaptor.forClass(Registration.class);
verify(registrationService).register(registration.capture(), any());
assertEquals("bob@example.com", registration.getValue().getEmail());
}
}
@@ -54,4 +54,19 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
assertNotNull(saved);
assertEquals("save@example.com", saved.getEmail());
}
// V19: a second account under the same email in other case is refused by the database too.
@Test
void emailsThatDifferOnlyInCase_AreOneAccount() {
entityManager.persist(RegisteredUserEntity.builder().email("dup@example.com").password("password")
.enabled(true).registrationDate(java.time.LocalDateTime.now()).build());
entityManager.flush();
assertTrue(registeredUserRepository.existsByEmail("Dup@Example.com"));
assertThrows(org.hibernate.exception.ConstraintViolationException.class, () -> {
entityManager.persist(RegisteredUserEntity.builder().email("Dup@example.com").password("password")
.enabled(true).registrationDate(java.time.LocalDateTime.now()).build());
entityManager.flush();
});
}
}
@@ -0,0 +1,16 @@
package auth_service.modules.user.persistence.mapper;
import auth_service.modules.user.persistence.entities.UserEntity;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class EntityMapperTest {
@Test
void mapToModel_UserWithoutSettingsRow_HasNoLanguage() {
UserEntity entity = UserEntity.builder().id(1L).email("a@b.c").role("USER").build();
assertThat(EntityMapper.mapToModel(entity).getSettings().getLanguage()).isEmpty();
}
}