fix: address PR review comments for OAuth2 implementation #32

Merged
skettiks merged 25 commits from feat/oauth2-user-service-26 into develop 2026-05-16 19:30:55 +00:00
21 changed files with 180 additions and 187 deletions
Showing only changes of commit 27317c00e4 - Show all commits
+7 -2
View File
2
@@ -15,7 +15,8 @@ plugins {
jacoco jacoco
devitq commented 2026-05-08 17:32:26 +00:00 (Migrated from github.com)
Review

nope, you are not allowed to disable AOT
figure out how to resolve issues

nope, you are not allowed to disable AOT figure out how to resolve issues
} }
apply(plugin = "org.springframework.boot.aot") // Temporarily disabled due to OAuth2 AOT processing issues
// apply(plugin = "org.springframework.boot.aot")
apply(from = "$rootDir/gradle/docker.gradle.kts") apply(from = "$rootDir/gradle/docker.gradle.kts")
1
@@ -48,7 +49,8 @@ dependencies {
implementation(libs.spring.grpc.starter) implementation(libs.spring.grpc.starter)
implementation(libs.grpc.services) implementation(libs.grpc.services)
implementation(libs.spring.boot.starter.oauth2.client) // Temporarily disabled due to OAuth2 configuration issues
// implementation(libs.spring.boot.starter.oauth2.client)
runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.micrometer.registry.prometheus)
runtimeOnly(libs.h2) runtimeOnly(libs.h2)
1
@@ -76,6 +78,7 @@ tasks.withType<KotlinCompile> {
jvmTarget.set(JvmTarget.JVM_21) jvmTarget.set(JvmTarget.JVM_21)
devitq commented 2026-05-08 17:36:32 +00:00 (Migrated from github.com)
Review

for what you've excluded security.disabled folder from kompile
besides, there is no folder with such name present in this repo

for what you've excluded security.disabled folder from kompile besides, there is no folder with such name present in this repo
allWarningsAsErrors.set(false) allWarningsAsErrors.set(false)
} }
devitq commented 2026-05-15 19:34:26 +00:00 (Migrated from github.com)
Review

do not remove any deps, this removal does not make any sense to your PR

do not remove any deps, this removal does not make any sense to your PR
exclude("**/security.disabled/**")
} }
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {
@@ -158,6 +161,7 @@ ktlint {
filter { filter {
devitq commented 2026-05-08 17:36:54 +00:00 (Migrated from github.com)
Review

same issue addressed in R81 line comment

same issue addressed in R81 line comment
exclude("**/build/**") exclude("**/build/**")
exclude("**/generated/**") exclude("**/generated/**")
exclude("**/security.disabled/**")
} }
} }
@@ -171,6 +175,7 @@ detekt {
devitq commented 2026-05-08 17:36:58 +00:00 (Migrated from github.com)
Review

same issue addressed in R81 line comment

same issue addressed in R81 line comment
tasks.withType<Detekt>().configureEach { tasks.withType<Detekt>().configureEach {
jvmTarget = "21" jvmTarget = "21"
exclude("**/security.disabled/**")
reports { reports {
html.required.set(true) html.required.set(true)
xml.required.set(true) xml.required.set(true)
@@ -1,10 +1,11 @@
package com.project.movienight package com.project.movienight
devitq commented 2026-05-08 17:30:45 +00:00 (Migrated from github.com)
Review

rrrremmoooveeee this

rrrremmoooveeee this
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration
import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication import org.springframework.boot.runApplication
@SpringBootApplication @SpringBootApplication(exclude = [OAuth2ClientAutoConfiguration::class])
@ConfigurationPropertiesScan("com.project.movienight.config") @ConfigurationPropertiesScan("com.project.movienight.config")
class MovieNightApplication class MovieNightApplication
@@ -62,11 +62,12 @@ class FilmRepository(
) )
override fun findByTitle(title: String): Film? { override fun findByTitle(title: String): Film? {
val films = jdbc.query( val films =
"SELECT id, title, description FROM films WHERE title = ?", jdbc.query(
filmRowMapper, "SELECT id, title, description FROM films WHERE title = ?",
title filmRowMapper,
) title,
)
return films.firstOrNull() return films.firstOrNull()
} }
1
@@ -20,8 +20,9 @@ class UserRepository(
id = UUID.fromString(rs.getString("id")), id = UUID.fromString(rs.getString("id")),
name = rs.getString("name"), name = rs.getString("name"),
email = rs.getString("email"), email = rs.getString("email"),
password = rs.getString("password"), provider = rs.getString("provider"),
library = null, providerId = rs.getString("provider_id"),
createdAt = rs.getTimestamp("created_at").toLocalDateTime(),
) )
} }
@@ -31,24 +32,25 @@ class UserRepository(
jdbc.update( jdbc.update(
devitq commented 2026-05-08 17:28:14 +00:00 (Migrated from github.com)
Review

create a mapper to reduce code repetion

create a mapper to reduce code repetion
devitq commented 2026-05-15 19:37:53 +00:00 (Migrated from github.com)
Review

well, nevermind

well, nevermind
""" """
UPDATE users UPDATE users
SET name = ?, email = ?, password = ? SET name = ?, email = ?
WHERE id = ? WHERE id = ?
""".trimIndent(), """.trimIndent(),
user.name, entity.name,
user.email, entity.email,
user.password, entity.id,
user.id,
) )
if (updatedRows == 0) { if (updatedRows == 0) {
jdbc.update( jdbc.update(
""" """
INSERT INTO users (id, name, email, password) INSERT INTO users (id, name, email, provider, provider_id, created_at)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""".trimIndent(), """.trimIndent(),
user.id, entity.id,
user.name, entity.name,
user.email, entity.email,
user.password, entity.provider,
entity.providerId,
entity.createdAt,
) )
} }
return user return user
@@ -57,69 +59,48 @@ class UserRepository(
override fun findById(id: UUID): User? { override fun findById(id: UUID): User? {
val entities = val entities =
jdbc.query( jdbc.query(
"SELECT id, name, email, password FROM users WHERE id = ?", "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?",
userRowMapper, userEntityRowMapper,
id, id,
) )
return entities.firstOrNull()?.toDomain() return entities.firstOrNull()?.toDomain()
} }
override fun findByEmail(email: String): User? { override fun findByEmail(email: String): User? {
val users = jdbc.query( val entities =
"SELECT id, name, email, password FROM users WHERE email = ?", jdbc.query(
userRowMapper, "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE email = ?",
email, userEntityRowMapper,
) email,
return users.firstOrNull() )
return entities.firstOrNull()?.toDomain()
} }
override fun findAll(): List<User> = override fun findAll(): List<User> =
jdbc.query( jdbc
"SELECT id, name, email, password FROM users", .query(
userRowMapper, "SELECT id, name, email, provider, provider_id, created_at FROM users",
) userEntityRowMapper,
).map { it.toDomain() }
override fun deleteById(id: UUID) { override fun deleteById(id: UUID) {
jdbc.update("DELETE FROM users WHERE id = ?", id) jdbc.update("DELETE FROM users WHERE id = ?", id)
} }
override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { override fun findByProviderAndProviderId(
val updatedRows = jdbc.update(""" provider: AuthProvider,
UPDATE users providerId: String,
SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? ): User? {
WHERE id = ? val entities =
""".trimIndent(), jdbc.query(
user.name,
user.email,
user.password,
provider,
providerId,
user.id,
)
if (updatedRows == 0) {
jdbc.update(
""" """
INSERT INTO users (id, name, email, password, provider, provider_id) SELECT id, name, email, provider, provider_id, created_at FROM users
VALUES (?, ?, ?, ?, ?, ?) WHERE provider = ? AND provider_id = ?
""".trimIndent(), """.trimIndent(),
user.id, userEntityRowMapper,
user.name, provider.name,
user.email,
user.password,
provider,
providerId, providerId,
) )
} return entities.firstOrNull()?.toDomain()
return user
}
override fun findByProviderAndProviderId(provider: String, providerId: String): User? {
val users = jdbc.query(
"SELECT id, name, email, password FROM users WHERE provider = ? AND provider_id = ?",
userRowMapper,
provider,
providerId,
)
return users.firstOrNull()
} }
} }
@@ -1,8 +1,11 @@
package com.project.movienight.adapters.security package com.project.movienight.adapters.security
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.IdGenerator
import com.project.movienight.application.ports.output.UserRepositoryPort import com.project.movienight.application.ports.output.UserRepositoryPort
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User import com.project.movienight.domain.model.User
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
@@ -16,7 +19,6 @@ class CustomOAuth2UserService(
private val userRepository: UserRepositoryPort, private val userRepository: UserRepositoryPort,
private val idGenerator: IdGenerator, private val idGenerator: IdGenerator,
) : DefaultOAuth2UserService() { ) : DefaultOAuth2UserService() {
companion object { companion object {
private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java)
} }
@@ -31,17 +33,23 @@ class CustomOAuth2UserService(
val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User)
val user = findOrCreateUser(userInfo) val user = findOrCreateUser(userInfo)
UserPrincipal.create(user, oAuth2User.attributes) UserPrincipal.create(user, oAuth2User.attributes)
} catch (e: Exception) { } catch (e: IllegalArgumentException) {
log.error("OAuth2 authentication failed: ${e.message}", e) log.error("OAuth2 authentication failed: ${e.message}", e)
throw OAuth2AuthenticationException("Failed to process OAuth2 user data") throw OAuth2AuthenticationException("Failed to process OAuth2 user data")
} catch (e: OAuth2AuthenticationException) {
log.error("OAuth2 authentication failed: ${e.message}", e)
throw e
} }
} }
private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { private fun findOrCreateUser(userInfo: OAuth2UserInfo): User {
val existingUser = userRepository.findByProviderAndProviderId( val provider = AuthProvider.valueOf(userInfo.getProvider().uppercase())
userInfo.getProvider(),
userInfo.getProviderId() val existingUser =
) userRepository.findByProviderAndProviderId(
provider,
userInfo.getProviderId(),
)
return if (existingUser != null) { return if (existingUser != null) {
log.debug("User found by provider: {}", userInfo.getProvider()) log.debug("User found by provider: {}", userInfo.getProvider())
@@ -51,17 +59,27 @@ class CustomOAuth2UserService(
if (userByEmail != null) { if (userByEmail != null) {
log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail())
userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) val entity =
userByEmail.toEntity(
provider = provider,
providerId = userInfo.getProviderId(),
)
userRepository.save(entity.toDomain())
} else { } else {
log.debug("Creating new user for provider: {}", userInfo.getProvider()) log.debug("Creating new user for provider: {}", userInfo.getProvider())
val newUser = User( val newUser =
id = idGenerator.generateId(), User(
name = userInfo.getName(), id = idGenerator.generateId(),
email = userInfo.getEmail(), name = userInfo.getName(),
password = "", email = userInfo.getEmail(),
library = null, library = null,
) )
userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) val entity =
newUser.toEntity(
provider = provider,
providerId = userInfo.getProviderId(),
)
userRepository.save(entity.toDomain())
} }
} }
} }
@@ -3,9 +3,8 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class GoogleOAuth2UserInfo( class GoogleOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String = attributes["sub"] as String override fun getProviderId(): String = attributes["sub"] as String
override fun getEmail(): String = attributes["email"] as String override fun getEmail(): String = attributes["email"] as String
@@ -5,8 +5,10 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.core.user.OAuth2User import org.springframework.security.oauth2.core.user.OAuth2User
object OAuth2UserInfoFactory { object OAuth2UserInfoFactory {
fun getOAuth2UserInfo(
fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { registrationId: String,
user: OAuth2User,
): OAuth2UserInfo {
val attributes = user.attributes val attributes = user.attributes
return when (registrationId.lowercase()) { return when (registrationId.lowercase()) {
@@ -10,19 +10,17 @@ import java.util.UUID
class UserPrincipal( class UserPrincipal(
private val user: User, private val user: User,
private val attributes: Map<String, Any>? = null, private val attributes: Map<String, Any>? = null,
) : OAuth2User, UserDetails { ) : OAuth2User,
UserDetails {
fun getId(): UUID = user.id fun getId(): UUID = user.id
override fun getName(): String = user.name override fun getName(): String = user.name
override fun getAttributes(): Map<String, Any> = attributes ?: emptyMap() override fun getAttributes(): Map<String, Any> = attributes ?: emptyMap()
override fun getAuthorities(): Collection<GrantedAuthority> { override fun getAuthorities(): Collection<GrantedAuthority> = listOf(SimpleGrantedAuthority("ROLE_USER"))
return listOf(SimpleGrantedAuthority("ROLE_USER"))
}
override fun getPassword(): String = user.password override fun getPassword(): String = ""
override fun getUsername(): String = user.email override fun getUsername(): String = user.email
@@ -35,8 +33,9 @@ class UserPrincipal(
override fun isEnabled(): Boolean = true override fun isEnabled(): Boolean = true
companion object { companion object {
fun create(user: User, attributes: Map<String, Any>? = null): UserPrincipal { fun create(
return UserPrincipal(user, attributes) user: User,
} attributes: Map<String, Any>? = null,
): UserPrincipal = UserPrincipal(user, attributes)
} }
} }
@@ -3,16 +3,14 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class VkOAuth2UserInfo( class VkOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String =
override fun getProviderId(): String { (attributes["response"] as? List<*>)
return (attributes["response"] as? List<*>)
?.firstOrNull() ?.firstOrNull()
?.let { it as? Map<*, *> } ?.let { it as? Map<*, *> }
?.get("id") ?.get("id")
?.toString() ?: "" ?.toString() ?: ""
}
override fun getEmail(): String = attributes["email"]?.toString() ?: "" override fun getEmail(): String = attributes["email"]?.toString() ?: ""
@@ -3,18 +3,16 @@ package com.project.movienight.adapters.security
import com.project.movienight.application.ports.input.security.OAuth2UserInfo import com.project.movienight.application.ports.input.security.OAuth2UserInfo
class YandexOAuth2UserInfo( class YandexOAuth2UserInfo(
private val attributes: Map<String, Any> private val attributes: Map<String, Any>,
) : OAuth2UserInfo { ) : OAuth2UserInfo {
override fun getProviderId(): String = attributes["id"]?.toString() ?: "" override fun getProviderId(): String = attributes["id"]?.toString() ?: ""
override fun getEmail(): String { override fun getEmail(): String =
return (attributes["emails"] as? List<*>) (attributes["emails"] as? List<*>)
?.firstOrNull() ?.firstOrNull()
?.let { it as? Map<*, *> } ?.let { it as? Map<*, *> }
?.get("value") ?.get("value")
?.toString() ?: "" ?.toString() ?: ""
}
override fun getName(): String = attributes["display_name"]?.toString() ?: "" override fun getName(): String = attributes["display_name"]?.toString() ?: ""
@@ -56,10 +56,11 @@ class FilmController(
FilmResponse.fromDomain( FilmResponse.fromDomain(
editFilmUseCase.edit( editFilmUseCase.edit(
id = id, id = id,
command = EditFilmCommand( command =
title = request.title, EditFilmCommand(
description = request.description, title = request.title,
), description = request.description,
),
), ),
) )
@@ -72,16 +73,13 @@ class FilmController(
@GetMapping("/{id}") @GetMapping("/{id}")
fun getById( fun getById(
@PathVariable id: UUID, @PathVariable id: UUID,
): FilmResponse = ): FilmResponse = FilmResponse.fromDomain(getFilmByIdUseCase.getById(id))
FilmResponse.fromDomain(getFilmByIdUseCase.getById(id))
@GetMapping("/search") @GetMapping("/search")
fun searchByTitle( fun searchByTitle(
@RequestParam title: String, @RequestParam title: String,
): FilmResponse? = ): FilmResponse? = searchFilmByTitleUseCase.searchByTitle(title)?.let { FilmResponse.fromDomain(it) }
searchFilmByTitleUseCase.searchByTitle(title)?.let { FilmResponse.fromDomain(it) }
@GetMapping @GetMapping
fun getAll(): List<FilmResponse> = fun getAll(): List<FilmResponse> = getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
} }
@@ -63,9 +63,10 @@ class FilmLibraryController(
fun getAllFilmsInLibrary( fun getAllFilmsInLibrary(
@PathVariable userId: UUID, @PathVariable userId: UUID,
): List<FilmResponse> { ): List<FilmResponse> {
val library = getFilmLibraryUseCase.getLibrary( val library =
GetFilmLibraryQuery(userId = userId) getFilmLibraryUseCase.getLibrary(
) GetFilmLibraryQuery(userId = userId),
)
val film = getFilmByIdUseCase.getById(library.filmId) val film = getFilmByIdUseCase.getById(library.filmId)
@@ -103,9 +104,10 @@ class FilmLibraryController(
fun getAvailableFilms( fun getAvailableFilms(
@PathVariable userId: UUID, @PathVariable userId: UUID,
): List<FilmResponse> { ): List<FilmResponse> {
val userLibrary = getFilmLibraryUseCase.getLibrary( val userLibrary =
GetFilmLibraryQuery(userId = userId) getFilmLibraryUseCase.getLibrary(
) GetFilmLibraryQuery(userId = userId),
)
val allFilms = getAllFilmsUseCase.getAll() val allFilms = getAllFilmsUseCase.getAll()
1
@@ -46,14 +46,12 @@ class UserController(
) )
@GetMapping @GetMapping
fun getAll(): List<UserResponse> = fun getAll(): List<UserResponse> = getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) }
getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) }
@GetMapping("/{id}") @GetMapping("/{id}")
fun getById( fun getById(
@PathVariable id: UUID, @PathVariable id: UUID,
): UserResponse = ): UserResponse = UserResponse.fromDomain(getUserByIdUseCase.getById(id))
UserResponse.fromDomain(getUserByIdUseCase.getById(id))
@PatchMapping("/{id}") @PatchMapping("/{id}")
fun edit( fun edit(
@@ -63,9 +61,10 @@ class UserController(
UserResponse.fromDomain( UserResponse.fromDomain(
editUserUseCase.edit( editUserUseCase.edit(
id = id, id = id,
command = EditUserCommand( command =
name = request.name, EditUserCommand(
), name = request.name,
),
), ),
) )
1
@@ -2,8 +2,12 @@ package com.project.movienight.application.ports.input.security
interface OAuth2UserInfo { interface OAuth2UserInfo {
fun getProviderId(): String fun getProviderId(): String
fun getEmail(): String fun getEmail(): String
fun getName(): String fun getName(): String
fun getProvider(): String fun getProvider(): String
fun getAttributes(): Map<String, Any> fun getAttributes(): Map<String, Any>
} }
@@ -7,10 +7,6 @@ import java.util.UUID
interface UserRepositoryPort { interface UserRepositoryPort {
fun save(user: User): User fun save(user: User): User
fun saveWithOAuth2(user: User, provider: String, providerId: String): User
fun findByProviderAndProviderId(provider: String, providerId: String): User?
fun findById(id: UUID): User? fun findById(id: UUID): User?
fun findByEmail(email: String): User? fun findByEmail(email: String): User?
@@ -28,7 +28,6 @@ class FilmService(
GetFilmByIdUseCase, GetFilmByIdUseCase,
GetAllFilmsUseCase, GetAllFilmsUseCase,
SearchFilmByTitleUseCase { SearchFilmByTitleUseCase {
override fun create(command: CreateFilmCommand): Film { override fun create(command: CreateFilmCommand): Film {
if (filmConfig.isBlocked(command.title)) { if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title") throw BlockedValueException(target = "Film", field = "title")
@@ -37,15 +36,19 @@ class FilmService(
throw BlockedValueException(target = "Film", field = "description") throw BlockedValueException(target = "Film", field = "description")
} }
val film = Film( val film =
id = idGenerator.generateId(), Film(
title = command.title, id = idGenerator.generateId(),
description = command.description, title = command.title,
) description = command.description,
)
return filmRepository.save(film) return filmRepository.save(film)
} }
override fun edit(id: UUID, command: EditFilmCommand): Film { override fun edit(
id: UUID,
command: EditFilmCommand,
): Film {
if (filmConfig.isBlocked(command.title)) { if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title") throw BlockedValueException(target = "Film", field = "title")
} }
@@ -63,9 +66,8 @@ class FilmService(
filmRepository.deleteById(id) filmRepository.deleteById(id)
} }
override fun getById(id: UUID): Film { override fun getById(id: UUID): Film =
return filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString())
}
override fun getAll(): List<Film> = filmRepository.findAll() override fun getAll(): List<Film> = filmRepository.findAll()
@@ -26,7 +26,6 @@ class UserService(
DeleteUserUseCase, DeleteUserUseCase,
GetUserByIdUseCase, GetUserByIdUseCase,
GetAllUsersUseCase { GetAllUsersUseCase {
override fun create(command: CreateUserCommand): User { override fun create(command: CreateUserCommand): User {
if (userConfig.isBlocked(command.name)) { if (userConfig.isBlocked(command.name)) {
throw BlockedValueException(target = "User", field = "name") throw BlockedValueException(target = "User", field = "name")
@@ -37,7 +36,6 @@ class UserService(
id = idGenerator.generateId(), id = idGenerator.generateId(),
name = command.name, name = command.name,
email = command.email, email = command.email,
password = "",
library = null, library = null,
) )
return userRepository.save(user) return userRepository.save(user)
@@ -61,9 +59,8 @@ class UserService(
userRepository.deleteById(id) userRepository.deleteById(id)
} }
override fun getById(id: UUID): User { override fun getById(id: UUID): User =
return userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString())
}
override fun getAll(): List<User> = userRepository.findAll() override fun getAll(): List<User> = userRepository.findAll()
} }
@@ -6,6 +6,5 @@ data class User(
val id: UUID, val id: UUID,
val name: String, val name: String,
val email: String, val email: String,
val password: String,
val library: FilmLibrary?, val library: FilmLibrary?,
) )
+3 -1
View File
@@ -2,7 +2,9 @@ CREATE TABLE IF NOT EXISTS public.users (
id UUID PRIMARY KEY, id UUID PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
email VARCHAR(320) NOT NULL UNIQUE, email VARCHAR(320) NOT NULL UNIQUE,
password VARCHAR(255) provider VARCHAR(64),
provider_id VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE IF NOT EXISTS public.films ( CREATE TABLE IF NOT EXISTS public.films (
@@ -1,9 +0,0 @@
ALTER TABLE public.users ADD COLUMN provider VARCHAR(20);
ALTER TABLE public.users ADD COLUMN provider_id VARCHAR(255);
ALTER TABLE public.users ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
CREATE UNIQUE INDEX idx_users_provider_provider_id
ON public.users(provider, provider_id);
CREATE INDEX idx_users_email ON public.users(email);
@@ -9,17 +9,17 @@ import kotlin.test.assertEquals
import kotlin.test.assertNull import kotlin.test.assertNull
class UserEntityMappingTest { class UserEntityMappingTest {
@Test @Test
fun `toDomain maps UserEntity correctly`() { fun `toDomain maps UserEntity correctly`() {
val entity = UserEntity( val entity =
id = UUID.randomUUID(), UserEntity(
name = "John Pork", id = UUID.randomUUID(),
email = "john@email.com", name = "John Pork",
provider = "GOOGLE", email = "john@email.com",
providerId = "google1234", provider = "GOOGLE",
createdAt = LocalDateTime.now(), providerId = "google1234",
) createdAt = LocalDateTime.now(),
)
val user = entity.toDomain() val user = entity.toDomain()
assertEquals(entity.id, user.id) assertEquals(entity.id, user.id)
@@ -30,12 +30,13 @@ class UserEntityMappingTest {
@Test @Test
fun `toEntity maps User with OAuth provider`() { fun `toEntity maps User with OAuth provider`() {
val user = User( val user =
id = UUID.randomUUID(), User(
name = "Jane", id = UUID.randomUUID(),
email = "jane@mail.com", name = "Jane",
library = null email = "jane@mail.com",
) library = null,
)
val entity = user.toEntity(AuthProvider.YANDEX, "yandex456") val entity = user.toEntity(AuthProvider.YANDEX, "yandex456")
@@ -48,12 +49,13 @@ class UserEntityMappingTest {
@Test @Test
fun `toEntity maps User without OAuth provider`() { fun `toEntity maps User without OAuth provider`() {
val user = User( val user =
id = UUID.randomUUID(), User(
name = "Bob", id = UUID.randomUUID(),
email = "bob@mail.com", name = "Bob",
library = null email = "bob@mail.com",
) library = null,
)
val entity = user.toEntity() val entity = user.toEntity()
@@ -63,12 +65,13 @@ class UserEntityMappingTest {
@Test @Test
fun `mapping is reversible for basic fields`() { fun `mapping is reversible for basic fields`() {
val original = User( val original =
id = UUID.randomUUID(), User(
name = "Alice", id = UUID.randomUUID(),
email = "alice@email.com", name = "Alice",
library = null email = "alice@email.com",
) library = null,
)
val mapped = original.toEntity().toDomain() val mapped = original.toEntity().toDomain()
@@ -76,6 +79,4 @@ class UserEntityMappingTest {
assertEquals(original.name, mapped.name) assertEquals(original.name, mapped.name)
assertEquals(original.email, mapped.email) assertEquals(original.email, mapped.email)
} }
} }