Public Access
refactor(users): login and registration use JdbcTemplate for users
This commit is contained in:
1 parent
b6be1879f0
commit
d082f3befd
12 files changed
+145
-262
No files matched your search
@@ -131,6 +131,9 @@ layers:
|
|||||||
| Domain | `model`, `service`, `exception` | Plain Java models and business logic. Uses repositories, never API classes, entities or `EntityMapper`. |
|
| Domain | `model`, `service`, `exception` | Plain Java models and business logic. Uses repositories, never API classes, entities or `EntityMapper`. |
|
||||||
| Persistence | `persistence`, `repo` | JPA entities, Spring Data repositories, `EntityMapper`; `repo` holds `JdbcTemplate` queries. Entities never reach the API layer. |
|
| Persistence | `persistence`, `repo` | JPA entities, Spring Data repositories, `EntityMapper`; `repo` holds `JdbcTemplate` queries. Entities never reach the API layer. |
|
||||||
|
|
||||||
|
`user` owns the tables `users` and `user_settings` through the one JPA entity `UserEntity`. `login` and
|
||||||
|
`registration` read and write `users` with `JdbcTemplate` (refresh tokens stay JPA). Every query for "a user who
|
||||||
|
may log in and is shown to others" uses `shared/account/ActiveUser`.
|
||||||
`profile` and `stats` only read data. They have no entities and use `JdbcTemplate` queries in a `repo` package.
|
`profile` and `stats` only read data. They have no entities and use `JdbcTemplate` queries in a `repo` package.
|
||||||
`version` has only an `api` package: `GET /api/version` answers the commit and build time of the running image.
|
`version` has only an `api` package: `GET /api/version` answers the commit and build time of the running image.
|
||||||
`account` (delete and nightly cleanup) sets the flag with `JdbcTemplate` in a `repo` package too, and calls Core
|
`account` (delete and nightly cleanup) sets the flag with `JdbcTemplate` in a `repo` package too, and calls Core
|
||||||
|
|||||||
+24
-29
@@ -1,13 +1,11 @@
|
|||||||
package auth_service.modules.login.persistence;
|
package auth_service.modules.login.persistence;
|
||||||
|
|
||||||
import auth_service.shared.account.EmailAddress;
|
|
||||||
import auth_service.modules.login.model.User;
|
import auth_service.modules.login.model.User;
|
||||||
import auth_service.modules.login.persistence.entities.MinimalUserEntity;
|
import auth_service.shared.account.EmailAddress;
|
||||||
import auth_service.modules.login.persistence.mapper.EntityMapper;
|
import auth_service.shared.security.Role;
|
||||||
import jakarta.persistence.EntityManager;
|
import lombok.RequiredArgsConstructor;
|
||||||
import jakarta.persistence.PersistenceContext;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import jakarta.persistence.TypedQuery;
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
@@ -15,39 +13,36 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@Transactional
|
@RequiredArgsConstructor
|
||||||
public class MinimalUserRepositoryImpl implements MinimalUserRepository {
|
public class MinimalUserRepositoryImpl implements MinimalUserRepository {
|
||||||
@PersistenceContext
|
|
||||||
private EntityManager entityManager;
|
private static final String SELECT_USER =
|
||||||
|
"SELECT id, email, password, steam_id, enabled, deleted_at IS NOT NULL AS deleted, role FROM users ";
|
||||||
|
|
||||||
|
private static final RowMapper<User> USER_ROW = (rs, row) -> User.builder()
|
||||||
|
.id(rs.getLong("id"))
|
||||||
|
.email(rs.getString("email"))
|
||||||
|
.password(rs.getString("password"))
|
||||||
|
.steamId(rs.getObject("steam_id", Long.class))
|
||||||
|
.enabled(rs.getBoolean("enabled"))
|
||||||
|
.deleted(rs.getBoolean("deleted"))
|
||||||
|
.role(Role.parse(rs.getString("role")))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> findByEmail(String email) {
|
public Optional<User> findByEmail(String email) {
|
||||||
var criteriaBuilder = entityManager.getCriteriaBuilder();
|
return jdbc.query(SELECT_USER + "WHERE email = ?", USER_ROW, EmailAddress.normalize(email)).stream().findFirst();
|
||||||
var userEntityCriteriaQuery = criteriaBuilder.createQuery(MinimalUserEntity.class);
|
|
||||||
var userEntity = userEntityCriteriaQuery.from(MinimalUserEntity.class);
|
|
||||||
|
|
||||||
userEntityCriteriaQuery.select(userEntity).where(criteriaBuilder.equal(
|
|
||||||
userEntity.get("email"), EmailAddress.normalize(email)));
|
|
||||||
|
|
||||||
TypedQuery<MinimalUserEntity> typedQuery = entityManager.createQuery(userEntityCriteriaQuery);
|
|
||||||
|
|
||||||
return typedQuery.getResultStream()
|
|
||||||
.findFirst()
|
|
||||||
.map(EntityMapper::mapToModel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> findById(long userId) {
|
public Optional<User> findById(long userId) {
|
||||||
return Optional.ofNullable(entityManager.find(MinimalUserEntity.class, userId)).map(EntityMapper::mapToModel);
|
return jdbc.query(SELECT_USER + "WHERE id = ?", USER_ROW, userId).stream().findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateLoginDate(long userId) {
|
public void updateLoginDate(long userId) {
|
||||||
var userEntity = entityManager.find(MinimalUserEntity.class, userId);
|
jdbc.update("UPDATE users SET last_login_date = ? WHERE id = ?", LocalDateTime.now(Clock.systemUTC()), userId);
|
||||||
|
|
||||||
if (userEntity != null) {
|
|
||||||
userEntity.setLastLoginDate(LocalDateTime.now(Clock.systemUTC()));
|
|
||||||
entityManager.flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package auth_service.modules.login.persistence.entities;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a user entity mapped to the 'users' table in PostgreSQL.
|
|
||||||
*/
|
|
||||||
@Entity
|
|
||||||
@Table(name = "users")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
public class MinimalUserEntity {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private boolean enabled;
|
|
||||||
|
|
||||||
@Column(name = "last_login_date")
|
|
||||||
private LocalDateTime lastLoginDate;
|
|
||||||
|
|
||||||
@Column(name = "steam_id")
|
|
||||||
private Long steamId;
|
|
||||||
|
|
||||||
@Column(name = "deleted_at", insertable = false, updatable = false)
|
|
||||||
private Instant deletedAt;
|
|
||||||
|
|
||||||
// Set by SQL only (the first admin).
|
|
||||||
@Column(name = "role", insertable = false, updatable = false)
|
|
||||||
private String role;
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package auth_service.modules.login.persistence.mapper;
|
package auth_service.modules.login.persistence.mapper;
|
||||||
|
|
||||||
import auth_service.modules.login.model.RefreshToken;
|
import auth_service.modules.login.model.RefreshToken;
|
||||||
import auth_service.modules.login.model.User;
|
|
||||||
import auth_service.modules.login.persistence.entities.MinimalUserEntity;
|
|
||||||
import auth_service.modules.login.persistence.entities.RefreshTokenEntity;
|
import auth_service.modules.login.persistence.entities.RefreshTokenEntity;
|
||||||
import auth_service.shared.security.Role;
|
import auth_service.shared.security.Role;
|
||||||
|
|
||||||
@@ -11,22 +9,6 @@ public abstract class EntityMapper {
|
|||||||
private EntityMapper() {
|
private EntityMapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static User mapToModel(MinimalUserEntity entity) {
|
|
||||||
User model = User.builder().build();
|
|
||||||
|
|
||||||
if (entity != null) {
|
|
||||||
model.setId(entity.getId());
|
|
||||||
model.setEmail(entity.getEmail());
|
|
||||||
model.setPassword(entity.getPassword());
|
|
||||||
model.setSteamId(entity.getSteamId());
|
|
||||||
model.setEnabled(entity.isEnabled());
|
|
||||||
model.setDeleted(entity.getDeletedAt() != null);
|
|
||||||
model.setRole(Role.parse(entity.getRole()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RefreshToken mapToModel(RefreshTokenEntity entity) {
|
public static RefreshToken mapToModel(RefreshTokenEntity entity) {
|
||||||
return new RefreshToken(entity.getId(), entity.getUserId(), entity.getFamilyId(), entity.getExpiresAt(),
|
return new RefreshToken(entity.getId(), entity.getUserId(), entity.getFamilyId(), entity.getExpiresAt(),
|
||||||
entity.getRotatedAt(), entity.getRevokedAt(),
|
entity.getRotatedAt(), entity.getRevokedAt(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package auth_service.modules.profile.repo;
|
package auth_service.modules.profile.repo;
|
||||||
|
|
||||||
import auth_service.modules.profile.model.PublicProfile;
|
import auth_service.modules.profile.model.PublicProfile;
|
||||||
|
import auth_service.shared.account.ActiveUser;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@@ -32,8 +33,8 @@ public class PublicProfileRepository {
|
|||||||
SELECT u.id, u.display_name, u.registration_date, s.profile_accent, s.profile_corners,
|
SELECT u.id, u.display_name, u.registration_date, s.profile_accent, s.profile_corners,
|
||||||
s.profile_banner_appid, s.profile_background_appid, s.profile_tiles
|
s.profile_banner_appid, s.profile_background_appid, s.profile_tiles
|
||||||
FROM users u JOIN user_settings s ON s.user_id = u.id
|
FROM users u JOIN user_settings s ON s.user_id = u.id
|
||||||
WHERE LOWER(u.display_name) = LOWER(?) AND u.enabled AND u.deleted_at IS NULL AND s.public_profile
|
WHERE LOWER(u.display_name) = LOWER(?) AND %s AND s.public_profile
|
||||||
""",
|
""".formatted(ActiveUser.of("u")),
|
||||||
(rs, row) -> new PublicProfile(rs.getLong("id"), rs.getString("display_name"),
|
(rs, row) -> new PublicProfile(rs.getLong("id"), rs.getString("display_name"),
|
||||||
rs.getTimestamp("registration_date") != null
|
rs.getTimestamp("registration_date") != null
|
||||||
? rs.getTimestamp("registration_date").toLocalDateTime().toLocalDate() : null,
|
? rs.getTimestamp("registration_date").toLocalDateTime().toLocalDate() : null,
|
||||||
|
|||||||
+23
-23
@@ -1,44 +1,44 @@
|
|||||||
package auth_service.modules.registration.persistence;
|
package auth_service.modules.registration.persistence;
|
||||||
|
|
||||||
import auth_service.shared.account.EmailAddress;
|
|
||||||
import auth_service.modules.registration.model.Registration;
|
import auth_service.modules.registration.model.Registration;
|
||||||
import auth_service.modules.registration.persistence.mapper.EntityMapper;
|
import auth_service.shared.account.EmailAddress;
|
||||||
import jakarta.persistence.EntityManager;
|
import lombok.RequiredArgsConstructor;
|
||||||
import jakarta.persistence.PersistenceContext;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import jakarta.transaction.Transactional;
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
@Transactional
|
@RequiredArgsConstructor
|
||||||
public class RegisteredUserRepositoryImpl implements RegisteredUserRepository {
|
public class RegisteredUserRepositoryImpl implements RegisteredUserRepository {
|
||||||
|
|
||||||
@PersistenceContext
|
private final JdbcTemplate jdbc;
|
||||||
private EntityManager entityManager;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean existsByEmail(String email) {
|
public boolean existsByEmail(String email) {
|
||||||
var jpql = "SELECT COUNT(u) FROM UserEntity u WHERE u.email = :email";
|
return Boolean.TRUE.equals(jdbc.queryForObject("SELECT EXISTS (SELECT 1 FROM users WHERE email = ?)",
|
||||||
var count = entityManager.createQuery(jpql, Long.class)
|
Boolean.class, EmailAddress.normalize(email)));
|
||||||
.setParameter("email", EmailAddress.normalize(email))
|
|
||||||
.getSingleResult();
|
|
||||||
|
|
||||||
return count != null && count > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean existsByDisplayName(String displayName) {
|
public boolean existsByDisplayName(String displayName) {
|
||||||
var jpql = "SELECT COUNT(u) FROM UserEntity u WHERE LOWER(u.displayName) = LOWER(:displayName)";
|
return Boolean.TRUE.equals(jdbc.queryForObject(
|
||||||
var count = entityManager.createQuery(jpql, Long.class)
|
"SELECT EXISTS (SELECT 1 FROM users WHERE LOWER(display_name) = LOWER(?))", Boolean.class, displayName));
|
||||||
.setParameter("displayName", displayName)
|
|
||||||
.getSingleResult();
|
|
||||||
|
|
||||||
return count != null && count > 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public long save(Registration registration) {
|
public long save(Registration registration) {
|
||||||
var userToRegisterEntity = EntityMapper.mapToPersistence(registration);
|
Long id = jdbc.queryForObject("""
|
||||||
entityManager.persist(userToRegisterEntity);
|
INSERT INTO users (email, password, enabled, display_name, registration_date)
|
||||||
return userToRegisterEntity.getId();
|
VALUES (?, ?, TRUE, ?, ?) RETURNING id""", Long.class,
|
||||||
|
registration.getEmail(), registration.getEncryptedPassword(), registration.getDisplayName(),
|
||||||
|
LocalDateTime.now(Clock.systemUTC()));
|
||||||
|
if (id == null) {
|
||||||
|
throw new IllegalStateException("No id for the new user");
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-38
@@ -1,38 +0,0 @@
|
|||||||
package auth_service.modules.registration.persistence.entities;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.*;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "users")
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Builder
|
|
||||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
|
||||||
public class RegisteredUserEntity {
|
|
||||||
@Id
|
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
||||||
private Long id;
|
|
||||||
|
|
||||||
@Column(unique = true, nullable = false)
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private String password;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private boolean enabled;
|
|
||||||
|
|
||||||
@Column(name = "display_name", length = 24)
|
|
||||||
private String displayName;
|
|
||||||
|
|
||||||
@Column(name = "registration_date", nullable = false)
|
|
||||||
private LocalDateTime registrationDate;
|
|
||||||
|
|
||||||
@Column(name = "last_login_date")
|
|
||||||
private LocalDateTime lastLoginDate;
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package auth_service.modules.registration.persistence.mapper;
|
|
||||||
|
|
||||||
import auth_service.modules.registration.model.Registration;
|
|
||||||
import auth_service.modules.registration.persistence.entities.RegisteredUserEntity;
|
|
||||||
|
|
||||||
import java.time.Clock;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
public abstract class EntityMapper {
|
|
||||||
|
|
||||||
private EntityMapper() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RegisteredUserEntity mapToPersistence(Registration userModel) {
|
|
||||||
return RegisteredUserEntity.builder().
|
|
||||||
email(userModel.getEmail()).
|
|
||||||
registrationDate(LocalDateTime.now(Clock.systemUTC())).
|
|
||||||
password(userModel.getEncryptedPassword()).
|
|
||||||
enabled(true).
|
|
||||||
displayName(userModel.getDisplayName()).
|
|
||||||
lastLoginDate(null).
|
|
||||||
build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package auth_service.modules.stats.repo;
|
package auth_service.modules.stats.repo;
|
||||||
|
|
||||||
|
import auth_service.shared.account.ActiveUser;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
@@ -15,13 +16,13 @@ public class StatsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public long countTotalUsers() {
|
public long countTotalUsers() {
|
||||||
Long v = jdbc.queryForObject("SELECT COUNT(*) FROM users WHERE enabled = TRUE AND deleted_at IS NULL", Long.class);
|
Long v = jdbc.queryForObject("SELECT COUNT(*) FROM users u WHERE " + ActiveUser.of("u"), Long.class);
|
||||||
return v != null ? v : 0L;
|
return v != null ? v : 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long countUsersWithSteamLinked() {
|
public long countUsersWithSteamLinked() {
|
||||||
Long v = jdbc.queryForObject(
|
Long v = jdbc.queryForObject(
|
||||||
"SELECT COUNT(*) FROM users WHERE enabled = TRUE AND deleted_at IS NULL AND steam_id IS NOT NULL", Long.class);
|
"SELECT COUNT(*) FROM users u WHERE " + ActiveUser.of("u") + " AND u.steam_id IS NOT NULL", Long.class);
|
||||||
return v != null ? v : 0L;
|
return v != null ? v : 0L;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package auth_service.shared.account;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SQL rule for a user who may log in and is shown to others: not blocked and not deleted.
|
||||||
|
*/
|
||||||
|
public final class ActiveUser {
|
||||||
|
|
||||||
|
private ActiveUser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String of(String alias) {
|
||||||
|
return alias + ".enabled AND " + alias + ".deleted_at IS NULL";
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
-39
@@ -1,13 +1,19 @@
|
|||||||
package auth_service.modules.login.persistence;
|
package auth_service.modules.login.persistence;
|
||||||
|
|
||||||
import auth_service.modules.login.persistence.entities.MinimalUserEntity;
|
import auth_service.modules.login.model.User;
|
||||||
|
import auth_service.shared.security.Role;
|
||||||
import auth_service.shared.test.AbstractPersistenceTest;
|
import auth_service.shared.test.AbstractPersistenceTest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
|
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@Import(MinimalUserRepositoryImpl.class)
|
@Import(MinimalUserRepositoryImpl.class)
|
||||||
class MinimalUserRepositoryTest extends AbstractPersistenceTest {
|
class MinimalUserRepositoryTest extends AbstractPersistenceTest {
|
||||||
@@ -16,53 +22,47 @@ class MinimalUserRepositoryTest extends AbstractPersistenceTest {
|
|||||||
private MinimalUserRepository minimalUserRepository;
|
private MinimalUserRepository minimalUserRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TestEntityManager entityManager;
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
@Test
|
private long insertUser(String email) {
|
||||||
void findByEmail_Success() {
|
Long id = jdbc.queryForObject("INSERT INTO users (email, password, enabled, registration_date) "
|
||||||
// Arrange
|
+ "VALUES (?, 'password', TRUE, ?) RETURNING id", Long.class, email, LocalDateTime.now());
|
||||||
var email = "minimal@example.com";
|
return id == null ? 0 : id;
|
||||||
var entity = MinimalUserEntity.builder()
|
|
||||||
.email(email)
|
|
||||||
.password("password")
|
|
||||||
.enabled(true)
|
|
||||||
.build();
|
|
||||||
entityManager.persist(entity);
|
|
||||||
entityManager.flush();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = minimalUserRepository.findByEmail(email);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
assertTrue(result.isPresent());
|
|
||||||
assertEquals(email, result.get().getEmail());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void updateLoginDate_Success() {
|
void findByEmail_MapsTheUser() {
|
||||||
// Arrange
|
long id = insertUser("minimal@example.com");
|
||||||
var entity = MinimalUserEntity.builder()
|
jdbc.update("UPDATE users SET steam_id = 76561198000000001, deleted_at = now() WHERE id = ?", id);
|
||||||
.email("login@example.com")
|
|
||||||
.password("password")
|
|
||||||
.enabled(true)
|
|
||||||
.build();
|
|
||||||
entity = entityManager.persist(entity);
|
|
||||||
entityManager.flush();
|
|
||||||
|
|
||||||
// Act
|
User user = minimalUserRepository.findByEmail("minimal@example.com").orElseThrow();
|
||||||
minimalUserRepository.updateLoginDate(entity.getId());
|
|
||||||
entityManager.clear(); // Clear cache to force reload from DB
|
|
||||||
|
|
||||||
// Assert
|
assertEquals(id, user.getId());
|
||||||
var updated = entityManager.find(MinimalUserEntity.class, entity.getId());
|
assertEquals("password", user.getPassword());
|
||||||
assertNotNull(updated.getLastLoginDate());
|
assertEquals(76561198000000001L, user.getSteamId());
|
||||||
|
assertTrue(user.isEnabled());
|
||||||
|
assertTrue(user.isDeleted());
|
||||||
|
assertEquals(Role.USER, user.getRole());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void findById_UnknownUser_IsEmpty() {
|
||||||
|
assertFalse(minimalUserRepository.findById(-1).isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateLoginDate_StoresTheDate() {
|
||||||
|
long id = insertUser("login@example.com");
|
||||||
|
|
||||||
|
minimalUserRepository.updateLoginDate(id);
|
||||||
|
|
||||||
|
assertNotNull(jdbc.queryForObject("SELECT last_login_date FROM users WHERE id = ?", LocalDateTime.class, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// An access token from before V19 still carries the email as it was typed.
|
// An access token from before V19 still carries the email as it was typed.
|
||||||
@Test
|
@Test
|
||||||
void findByEmail_IgnoresCase() {
|
void findByEmail_IgnoresCase() {
|
||||||
entityManager.persist(MinimalUserEntity.builder().email("case@example.com").password("password").enabled(true).build());
|
insertUser("case@example.com");
|
||||||
entityManager.flush();
|
|
||||||
|
|
||||||
assertTrue(minimalUserRepository.findByEmail("Case@Example.com").isPresent());
|
assertTrue(minimalUserRepository.findByEmail("Case@Example.com").isPresent());
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-41
@@ -1,14 +1,19 @@
|
|||||||
package auth_service.modules.registration.persistence;
|
package auth_service.modules.registration.persistence;
|
||||||
|
|
||||||
import auth_service.modules.registration.model.Registration;
|
import auth_service.modules.registration.model.Registration;
|
||||||
import auth_service.modules.registration.persistence.entities.RegisteredUserEntity;
|
|
||||||
import auth_service.shared.test.AbstractPersistenceTest;
|
import auth_service.shared.test.AbstractPersistenceTest;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.jpa.test.autoconfigure.TestEntityManager;
|
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
@Import(RegisteredUserRepositoryImpl.class)
|
@Import(RegisteredUserRepositoryImpl.class)
|
||||||
class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
|
class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
|
||||||
@@ -17,59 +22,49 @@ class RegisteredUserRepositoryTest extends AbstractPersistenceTest {
|
|||||||
private RegisteredUserRepository registeredUserRepository;
|
private RegisteredUserRepository registeredUserRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TestEntityManager entityManager;
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
private static Registration registration(String email, String displayName) {
|
||||||
|
return Registration.builder().email(email).encryptedPassword("hashed").displayName(displayName).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void existsByEmail_Success() {
|
void existsByEmail_OnlyForAStoredEmail() {
|
||||||
// Arrange
|
registeredUserRepository.save(registration("exists@example.com", null));
|
||||||
var email = "exists@example.com";
|
|
||||||
var entity = RegisteredUserEntity.builder()
|
|
||||||
.email(email)
|
|
||||||
.password("password")
|
|
||||||
.enabled(true)
|
|
||||||
.registrationDate(java.time.LocalDateTime.now())
|
|
||||||
.build();
|
|
||||||
entityManager.persist(entity);
|
|
||||||
entityManager.flush();
|
|
||||||
|
|
||||||
// Act & Assert
|
assertTrue(registeredUserRepository.existsByEmail("exists@example.com"));
|
||||||
assertTrue(registeredUserRepository.existsByEmail(email));
|
|
||||||
assertFalse(registeredUserRepository.existsByEmail("other@example.com"));
|
assertFalse(registeredUserRepository.existsByEmail("other@example.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void save_Success() {
|
void existsByDisplayName_IgnoresCase() {
|
||||||
// Arrange
|
registeredUserRepository.save(registration("named@example.com", "Lobo"));
|
||||||
var registration = Registration.builder()
|
|
||||||
.email("save@example.com")
|
|
||||||
.encryptedPassword("hashed")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Act
|
assertTrue(registeredUserRepository.existsByDisplayName("lobo"));
|
||||||
long id = registeredUserRepository.save(registration);
|
assertFalse(registeredUserRepository.existsByDisplayName("Wolf"));
|
||||||
|
}
|
||||||
|
|
||||||
// Assert
|
@Test
|
||||||
assertTrue(id > 0);
|
void save_StoresAnEnabledUser() {
|
||||||
var saved = entityManager.find(RegisteredUserEntity.class, id);
|
long id = registeredUserRepository.save(registration("save@example.com", "Saver"));
|
||||||
assertNotNull(saved);
|
|
||||||
assertEquals("save@example.com", saved.getEmail());
|
Map<String, Object> saved = jdbc.queryForMap("SELECT * FROM users WHERE id = ?", id);
|
||||||
|
assertEquals("save@example.com", saved.get("email"));
|
||||||
|
assertEquals("hashed", saved.get("password"));
|
||||||
|
assertEquals(true, saved.get("enabled"));
|
||||||
|
assertEquals("Saver", saved.get("display_name"));
|
||||||
|
assertEquals("USER", saved.get("role"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// V19: a second account under the same email in other case is refused by the database too.
|
// V19: a second account under the same email in other case is refused by the database too.
|
||||||
@Test
|
@Test
|
||||||
void emailsThatDifferOnlyInCase_AreOneAccount() {
|
void emailsThatDifferOnlyInCase_AreOneAccount() {
|
||||||
entityManager.persist(RegisteredUserEntity.builder().email("dup@example.com").password("password")
|
registeredUserRepository.save(registration("dup@example.com", null));
|
||||||
.enabled(true).registrationDate(java.time.LocalDateTime.now()).build());
|
|
||||||
entityManager.flush();
|
|
||||||
|
|
||||||
assertTrue(registeredUserRepository.existsByEmail("Dup@Example.com"));
|
assertTrue(registeredUserRepository.existsByEmail("Dup@Example.com"));
|
||||||
var refused = assertThrows(org.hibernate.exception.ConstraintViolationException.class, () -> {
|
DuplicateKeyException refused = assertThrows(DuplicateKeyException.class,
|
||||||
entityManager.persist(RegisteredUserEntity.builder().email("Dup@example.com").password("password")
|
() -> registeredUserRepository.save(registration("Dup@example.com", null)));
|
||||||
.enabled(true).registrationDate(java.time.LocalDateTime.now()).build());
|
// GlobalExceptionController finds the name in the message, in every server language.
|
||||||
entityManager.flush();
|
assertTrue(refused.getMostSpecificCause().getMessage().contains("users_email_lower"));
|
||||||
});
|
|
||||||
// GlobalExceptionController decides on the state; on an English server Hibernate also gives the name.
|
|
||||||
assertEquals("23505", refused.getSQLState());
|
|
||||||
assertEquals("users_email_lower", refused.getConstraintName());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user