feat(user): add welcome-seen-at timestamp for welcome page tracking

Null means the welcome page was not seen yet. Existing users are backfilled
at migration time so only newly registered users see the page.
This commit is contained in:
lobo committed 2026-10-01 10:44:22 +02:00
1 parent 2b2f45b9af
commit 6b6ac44551
12 files changed
+105 -1

No files matched your search

@@ -33,9 +33,11 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.Matchers.nullValue;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
@@ -381,6 +383,40 @@ class UserControllerTest extends AbstractWebMvcTest {
.andExpect(jsonPath("$.settings.favoritePlatform").value("PS5"));
}
@Test
void getCurrentUser_SendsWelcomeSeenAtNullForANewUser() throws Exception {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUser(1L)).thenReturn(User.builder().email("test@example.com")
.settings(UserSettings.builder().language("en").theme("dark").build()).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.settings.welcomeSeenAt").value(nullValue()));
}
@Test
void getCurrentUser_SendsWelcomeSeenAtAsIsoInstant() throws Exception {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(new UserPrincipal("1", null), null, Collections.emptyList())
);
when(userService.getUser(1L)).thenReturn(User.builder().email("test@example.com")
.settings(UserSettings.builder().language("en").theme("dark")
.welcomeSeenAt(Instant.parse("2026-10-01T10:00:00Z")).build()).build());
mockMvc.perform(get("/api/users/me").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.settings.welcomeSeenAt").value("2026-10-01T10:00:00Z"));
}
@Test
void markWelcomeSeen_Returns204() throws Exception {
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
new UserPrincipal("1", null), null, Collections.emptyList()));
mockMvc.perform(put("/api/users/settings/welcome-seen")).andExpect(status().isNoContent());
verify(userService).markWelcomeSeen(1L);
}
@Test
void getCurrentUser_NeverChangedDashboard_SendsNullTiles() throws Exception {
String email = "test@example.com";
@@ -14,7 +14,9 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
import org.springframework.jdbc.core.JdbcTemplate;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
@@ -374,6 +376,28 @@ class UserRepositoryTest extends AbstractPersistenceTest {
assertEquals(profile, findByEmail("longtile@example.com").orElseThrow().getSettings().getProfileTiles());
}
@Test
void markWelcomeSeen_IsNullForANewRowAndSurvivesASettingsSave() {
UserEntity userEntity = persisted(UserEntity.builder()
.email("welcome@example.com")
.password("password")
.enabled(true)
.registrationDate(LocalDateTime.now())
.build());
assertNull(findByEmail("welcome@example.com").orElseThrow().getSettings().getWelcomeSeenAt());
Instant seen = Instant.now().truncatedTo(ChronoUnit.MILLIS);
userRepository.update(userEntity.getId(), user -> user.getSettings().setWelcomeSeenAt(seen));
entityManager.flush();
entityManager.clear();
assertEquals(seen, findByEmail("welcome@example.com").orElseThrow().getSettings().getWelcomeSeenAt());
updateSettings(userEntity.getId(), null, UserSettings.builder().language("de").theme("dark").build());
entityManager.flush();
entityManager.clear();
assertEquals(seen, findByEmail("welcome@example.com").orElseThrow().getSettings().getWelcomeSeenAt());
}
private void updateSettings(long userId, String displayName, UserSettings settings) {
userRepository.update(userId, user -> {
user.setDisplayName(displayName);
@@ -19,6 +19,8 @@ import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
@@ -26,6 +28,7 @@ import java.util.function.Consumer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
@@ -192,6 +195,14 @@ class UserServiceTest extends AbstractUnitTest {
assertEquals("Switch", appliedChanges(1).getFirst().getSettings().getFavoritePlatform());
}
@Test
void markWelcomeSeen_StampsNow() {
userService.markWelcomeSeen(1L);
Instant stamp = appliedChanges(1).getFirst().getSettings().getWelcomeSeenAt();
assertTrue(Duration.between(stamp, Instant.now()).abs().getSeconds() < 5);
}
/**
* Runs each change user 1 got on a fresh user with default settings.
*/