fix(settings): give every user a settings row

V20 adds the row for old users, registration inserts one. The mapper fallback (en, system) disagreed with the column defaults.
This commit is contained in:
lobo committed 2026-09-30 13:41:56 +02:00
1 parent d082f3befd
commit fb51f836a8
16 files changed
+97 -171

No files matched your search

@@ -42,6 +42,7 @@ class ExpiredAccessTokenTest extends AbstractIntegrationTest {
jdbc.update("DELETE FROM users WHERE email = ?", EMAIL);
jdbc.update("INSERT INTO users (email, password, enabled, steam_id) VALUES (?, ?, true, NULL)",
EMAIL, passwordEncoder.encode(PASSWORD));
addSettingsRows();
}
private String expiredAccessCookie() throws Exception {
@@ -63,6 +63,7 @@ class SessionFlowTest extends AbstractIntegrationTest {
jdbc.update("DELETE FROM users WHERE email = ?", EMAIL);
jdbc.update("INSERT INTO users (email, password, enabled, steam_id) VALUES (?, ?, true, NULL)",
EMAIL, passwordEncoder.encode(PASSWORD));
addSettingsRows();
}
private MockHttpServletResponse login() throws Exception {
@@ -61,6 +61,7 @@ class SteamLinkFlowTest extends AbstractIntegrationTest {
jdbc.update("INSERT INTO users (email, password, enabled) VALUES (?, ?, true)", EMAIL, passwordEncoder.encode(PASSWORD));
// Someone who typed the same ID in before Steam linking existed.
jdbc.update("INSERT INTO users (email, password, enabled, steam_id) VALUES (?, 'x', true, ?)", TYPIST, STEAM_ID);
addSettingsRows();
when(steamOpenIdService.loginUrl(anyString(), anyString())).thenReturn("https://steamcommunity.com/openid/login?test");
}
@@ -59,6 +59,7 @@ class DeletedAccountFlowTest extends AbstractIntegrationTest {
deleteAccount(access, PASSWORD).andExpect(status().isNoContent());
Long newUser = jdbc.queryForObject("INSERT INTO users (email, password, enabled) VALUES (?, 'x', true) RETURNING id",
Long.class, newEmail);
addSettingsRows();
try {
assertThat(userService.linkVerifiedSteamId(newUser, steamId)).isEqualTo(SteamLinkOutcome.LINKED);
@@ -48,8 +48,10 @@ class AdminFlowTest extends AbstractIntegrationTest {
private long insertUser(String email, String role) {
jdbc.update("DELETE FROM users WHERE email = ?", email);
return jdbc.queryForObject("INSERT INTO users (email, password, enabled, role, display_name) VALUES (?, ?, true, ?, ?) "
Long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, role, display_name) VALUES (?, ?, true, ?, ?) "
+ "RETURNING id", Long.class, email, passwordEncoder.encode(PASSWORD), role, email.substring(0, 16));
addSettingsRows();
return id;
}
@BeforeEach
@@ -45,6 +45,7 @@ class HintFlowTest extends AbstractIntegrationTest {
jdbc.update("DELETE FROM users WHERE email = ?", email);
jdbc.update("INSERT INTO users (email, password, enabled, role, display_name) VALUES (?, ?, true, ?, ?)",
email, passwordEncoder.encode(PASSWORD), role, email.substring(0, 16));
addSettingsRows();
}
@BeforeEach
@@ -56,6 +56,16 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
assertEquals("USER", saved.get("role"));
}
@Test
void save_CreatesTheSettingsRowWithTheColumnDefaults() {
long id = registeredUserRepository.save(registration("row@example.com", null));
Map<String, Object> settings = jdbc.queryForMap("SELECT * FROM user_settings WHERE user_id = ?", id);
assertEquals("", settings.get("language"));
assertEquals("light", settings.get("theme"));
assertEquals(false, settings.get("public_profile"));
}
// V19: a second account under the same email in other case is refused by the database too.
@Test
void emailsThatDifferOnlyInCase_AreOneAccount() {
@@ -43,9 +43,26 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.flatMap(userRepository::findById);
}
// Registration inserts the settings row in production; the entity alone does not.
private UserEntity persisted(UserEntity user) {
entityManager.persist(user);
jdbc.update("INSERT INTO user_settings (user_id) VALUES (?)", user.getId());
entityManager.clear();
return user;
}
@Test
void findById_SettingsRowMissing_Throws() {
UserEntity user = entityManager.persistFlushFind(UserEntity.builder().email("norow@example.com")
.password("password").enabled(true).registrationDate(LocalDateTime.now()).build());
entityManager.clear();
assertThrows(IllegalStateException.class, () -> userRepository.findById(user.getId()));
}
@Test
void updateSettings_KeepsADeleteThatCommittedAfterTheLoad() {
var user = entityManager.persistFlushFind(UserEntity.builder().email("race@example.com").password("password")
var user = persisted(UserEntity.builder().email("race@example.com").password("password")
.enabled(true).registrationDate(LocalDateTime.now()).build());
// The delete request commits after the settings request loaded the user.
jdbc.update("UPDATE users SET deleted_at = NOW() WHERE id = ?", user.getId());
@@ -67,8 +84,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.steamId(123456L)
.registrationDate(LocalDateTime.now())
.build();
entityManager.persist(userEntity);
entityManager.flush();
persisted(userEntity);
// Act
var result = findByEmail(email);
@@ -89,8 +105,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.steamId(123456L)
.registrationDate(LocalDateTime.now())
.build();
userEntity = entityManager.persist(userEntity);
entityManager.flush();
userEntity = persisted(userEntity);
var settings = UserSettings.builder()
.language("de")
@@ -112,13 +127,12 @@ class UserRepositoryTest extends AbstractPersistenceTest {
void findByEmail_NoSteamId() {
// Arrange
var email = "nosteam@example.com";
entityManager.persist(UserEntity.builder()
persisted(UserEntity.builder()
.email(email)
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
// Act
var result = findByEmail(email);
@@ -129,7 +143,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
}
private UserEntity user(String email, Long steamId, boolean verified) {
UserEntity user = entityManager.persist(UserEntity.builder()
UserEntity user = persisted(UserEntity.builder()
.email(email)
.password("password")
.enabled(true)
@@ -137,7 +151,6 @@ class UserRepositoryTest extends AbstractPersistenceTest {
.steamIdVerified(verified)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
return user;
}
@@ -173,7 +186,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void setVerifiedSteamId_DeletedUser_IsRefused() {
var deleted = entityManager.persistFlushFind(UserEntity.builder().email("deleted@example.com").password("password")
var deleted = persisted(UserEntity.builder().email("deleted@example.com").password("password")
.enabled(true).registrationDate(LocalDateTime.now()).build());
jdbc.update("UPDATE users SET deleted_at = NOW() WHERE id = ?", deleted.getId());
@@ -196,19 +209,18 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void displayName_StoredAndCheckedCaseInsensitive() {
// Arrange
var owner = entityManager.persist(UserEntity.builder()
var owner = persisted(UserEntity.builder()
.email("owner2@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
var other = entityManager.persist(UserEntity.builder()
var other = persisted(UserEntity.builder()
.email("other2@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
var settings = UserSettings.builder().language("en").theme("light").build();
// Act
@@ -224,13 +236,12 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void updateSettings_StoresPublicProfile_AndFindByEmailReadsIt() {
var userEntity = entityManager.persist(UserEntity.builder()
var userEntity = persisted(UserEntity.builder()
.email("public@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
userRepository.updateSettings(userEntity.getId(), "Lobo",
UserSettings.builder().language("de").theme("dark").publicProfile(true).build());
@@ -240,33 +251,14 @@ class UserRepositoryTest extends AbstractPersistenceTest {
assertTrue(findByEmail("public@example.com").orElseThrow().getSettings().isPublicProfile());
}
@Test
void updateAppearance_WithoutSettingsRow_CreatesItAndFindByEmailReadsIt() {
var userEntity = entityManager.persist(UserEntity.builder()
.email("appearance@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
userRepository.updateAppearance(userEntity.getId(), new Appearance("orange", "round", true, 620L, 1086940L, false));
entityManager.flush();
entityManager.clear();
var settings = findByEmail("appearance@example.com").orElseThrow().getSettings();
assertEquals(new Appearance("orange", "round", true, 620L, 1086940L, false), settings.getAppearance());
}
@Test
void updateSettings_KeepsTheAppearance() {
var userEntity = entityManager.persist(UserEntity.builder()
var userEntity = persisted(UserEntity.builder()
.email("keep@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
userRepository.updateAppearance(userEntity.getId(), new Appearance("pink", "sharp", false, 220L, null, true));
entityManager.flush();
@@ -279,29 +271,14 @@ class UserRepositoryTest extends AbstractPersistenceTest {
assertEquals(new Appearance("pink", "sharp", false, 220L, null, true), settings.getAppearance());
}
@Test
void findByEmail_WithoutSettingsRow_HasTheDefaultAppearance() {
entityManager.persist(UserEntity.builder()
.email("defaults@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
assertEquals(Appearance.DEFAULT, findByEmail("defaults@example.com").orElseThrow()
.getSettings().getAppearance());
}
@Test
void updateProfileStyle_IsReadBackAndStartsWithTheDefaults() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
UserEntity userEntity = persisted(UserEntity.builder()
.email("style@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
assertEquals(ProfileStyle.DEFAULT, findByEmail("style@example.com").orElseThrow()
.getSettings().getProfileStyle());
@@ -315,13 +292,12 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void updateTiles_AreReadBackAndKeptByUpdateSettings() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
UserEntity userEntity = persisted(UserEntity.builder()
.email("tiles@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
entityManager.flush();
UserSettings fresh = findByEmail("tiles@example.com").orElseThrow().getSettings();
assertNull(fresh.getDashboardTiles());
assertEquals(UserSettings.DEFAULT_PROFILE_TILES, fresh.getProfileTiles());
@@ -343,7 +319,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void updateFavoritePlatform_StoresAndClears() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
UserEntity userEntity = persisted(UserEntity.builder()
.email("favorite@example.com")
.password("password")
.enabled(true)
@@ -365,7 +341,7 @@ class UserRepositoryTest extends AbstractPersistenceTest {
@Test
void updateDashboardTiles_NullResetsToTheDefault() {
UserEntity userEntity = entityManager.persist(UserEntity.builder()
UserEntity userEntity = persisted(UserEntity.builder()
.email("reset@example.com")
.password("password")
.enabled(true)
@@ -1,16 +0,0 @@
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();
}
}
@@ -1,7 +1,9 @@
package auth_service.shared.test;
import org.junit.jupiter.api.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.test.context.DynamicPropertyRegistry;
@@ -33,6 +35,17 @@ public abstract class AbstractIntegrationTest {
postgres.start();
}
@Autowired
private JdbcTemplate baseJdbc;
/**
* For users a test inserts by SQL. Registration inserts the row itself.
*/
protected void addSettingsRows() {
baseJdbc.update("INSERT INTO user_settings (user_id) SELECT u.id FROM users u "
+ "WHERE NOT EXISTS (SELECT 1 FROM user_settings s WHERE s.user_id = u.id)");
}
// No key is committed: a fresh one per JVM.
private static final KeyPair KEYS = generateKeys();