From 49f74d1898b1da8aee3e0deebcdad9b04d2c6628 Mon Sep 17 00:00:00 2001 From: skettiks Date: Mon, 20 Apr 2026 19:45:55 +0300 Subject: [PATCH 1/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20User,=20Film,=20FilmLibrary=20-=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B5?= =?UTF-8?q?=D0=B2=20application-test.yaml:=20=D0=BD=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=20=D1=82=D0=B5=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BE=D0=BA=D1=80=D1=83=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FilmLibraryRepositoryIntegrationTest.kt | 219 ++++++++++++++++++ .../jdbc/FilmRepositoryIntegrationTest.kt | 152 ++++++++++++ .../jdbc/UserRepositoryIntegrationTest.kt | 159 +++++++++++++ src/test/resources/application-test.yaml | 28 +++ 4 files changed, 558 insertions(+) create mode 100644 src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt create mode 100644 src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt create mode 100644 src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt create mode 100644 src/test/resources/application-test.yaml diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt new file mode 100644 index 0000000..232216a --- /dev/null +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt @@ -0,0 +1,219 @@ +package com.project.movienight.adapters.persistence.jdbc + +import com.project.movienight.domain.model.Film +import com.project.movienight.domain.model.FilmLibrary +import com.project.movienight.domain.model.User +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +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 java.util.UUID + +@SpringBootTest +@ActiveProfiles("test") +class FilmLibraryRepositoryIntegrationTest { + @Autowired + private lateinit var filmLibraryRepository: FilmLibraryRepository + + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var filmRepository: FilmRepository + + @Autowired + private lateinit var jdbcTemplate: JdbcTemplate + + private lateinit var testUser: User + private lateinit var testFilm: Film + + @BeforeEach + fun setup() { + cleanDatabase() + createTestData() + } + + @AfterEach + fun cleanup() { + cleanDatabase() + } + + private fun cleanDatabase() { + jdbcTemplate.execute("DELETE FROM favorites") + jdbcTemplate.execute("DELETE FROM films") + jdbcTemplate.execute("DELETE FROM users") + } + + private fun createTestData() { + testUser = User(UUID.randomUUID(), "Иван Иванов", "ivan@example.com", null) + testFilm = Film(UUID.randomUUID(), "Начало", "Захватывающий триллер") + userRepository.save(testUser) + filmRepository.save(testFilm) + } + + @Test + fun `should save new film library entry and return saved entry`() { + val entry = FilmLibrary( + id = UUID.randomUUID(), + userId = testUser.id, + filmId = testFilm.id, + comment = "Отличный фильм!", + isViewed = false, + ) + + val savedEntry = filmLibraryRepository.save(entry) + + assertNotNull(savedEntry) + assertEquals(entry.id, savedEntry.id) + assertEquals(entry.userId, savedEntry.userId) + assertEquals(entry.filmId, savedEntry.filmId) + assertEquals(entry.comment, savedEntry.comment) + assertEquals(entry.isViewed, savedEntry.isViewed) + } + + @Test + fun `should update existing film library entry`() { + val entryId = UUID.randomUUID() + val originalEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Хочу посмотреть", false) + filmLibraryRepository.save(originalEntry) + + val updatedEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true) + val result = filmLibraryRepository.save(updatedEntry) + + assertEquals(entryId, result.id) + assertEquals("Уже посмотрел, потрясающе!", result.comment) + assertTrue(result.isViewed) + + val foundEntry = filmLibraryRepository.findById(entryId) + assertNotNull(foundEntry) + assertEquals("Уже посмотрел, потрясающе!", foundEntry?.comment) + assertTrue(foundEntry?.isViewed ?: false) + } + + @Test + fun `should find film library entry by id`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false) + filmLibraryRepository.save(entry) + + val foundEntry = filmLibraryRepository.findById(entry.id) + + assertNotNull(foundEntry) + assertEquals(entry.id, foundEntry?.id) + assertEquals(entry.userId, foundEntry?.userId) + assertEquals(entry.filmId, foundEntry?.filmId) + assertEquals(entry.comment, foundEntry?.comment) + assertEquals(entry.isViewed, foundEntry?.isViewed) + } + + @Test + fun `should return null when film library entry not found by id`() { + val nonExistentId = UUID.randomUUID() + + val foundEntry = filmLibraryRepository.findById(nonExistentId) + + assertNull(foundEntry) + } + + @Test + fun `should find all film library entries`() { + val entry1 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 1", false) + val entry2 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 2", true) + val entry3 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false) + + filmLibraryRepository.save(entry1) + filmLibraryRepository.save(entry2) + filmLibraryRepository.save(entry3) + + val allEntries = filmLibraryRepository.findAll() + + assertEquals(3, allEntries.size) + assertTrue(allEntries.any { it.id == entry1.id }) + assertTrue(allEntries.any { it.id == entry2.id }) + assertTrue(allEntries.any { it.id == entry3.id }) + } + + @Test + fun `should return empty list when no film library entries exist`() { + val allEntries = filmLibraryRepository.findAll() + + assertTrue(allEntries.isEmpty()) + } + + @Test + fun `should delete film library entry by id`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Для удаления", false) + filmLibraryRepository.save(entry) + + filmLibraryRepository.deleteById(entry.id) + + val foundEntry = filmLibraryRepository.findById(entry.id) + assertNull(foundEntry) + } + + @Test + fun `should not throw exception when deleting non-existent entry`() { + val nonExistentId = UUID.randomUUID() + + filmLibraryRepository.deleteById(nonExistentId) + } + + @Test + fun `should save entry with null comment`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false) + + val savedEntry = filmLibraryRepository.save(entry) + + assertNotNull(savedEntry) + assertNull(savedEntry.comment) + } + + @Test + fun `should save entry with isViewed true`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true) + + val savedEntry = filmLibraryRepository.save(entry) + + assertNotNull(savedEntry) + assertTrue(savedEntry.isViewed) + } + + @Test + fun `should save entry with isViewed false`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false) + + val savedEntry = filmLibraryRepository.save(entry) + + assertNotNull(savedEntry) + assertFalse(savedEntry.isViewed) + } + + @Test + fun `should cascade delete entries when user is deleted`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false) + filmLibraryRepository.save(entry) + + userRepository.deleteById(testUser.id) + + val foundEntry = filmLibraryRepository.findById(entry.id) + assertNull(foundEntry) + } + + @Test + fun `should cascade delete entries when film is deleted`() { + val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Запись о фильме", false) + filmLibraryRepository.save(entry) + + filmRepository.deleteById(testFilm.id) + + val foundEntry = filmLibraryRepository.findById(entry.id) + assertNull(foundEntry) + } +} diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt new file mode 100644 index 0000000..926177b --- /dev/null +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt @@ -0,0 +1,152 @@ +package com.project.movienight.adapters.persistence.jdbc + +import com.project.movienight.domain.model.Film +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +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 java.util.UUID + +@SpringBootTest +@ActiveProfiles("test") +class FilmRepositoryIntegrationTest { + @Autowired + private lateinit var filmRepository: FilmRepository + + @Autowired + private lateinit var jdbcTemplate: JdbcTemplate + + @BeforeEach + fun setup() { + cleanDatabase() + } + + @AfterEach + fun cleanup() { + cleanDatabase() + } + + private fun cleanDatabase() { + jdbcTemplate.execute("DELETE FROM favorites") + jdbcTemplate.execute("DELETE FROM films") + jdbcTemplate.execute("DELETE FROM users") + } + + @Test + fun `should save new film and return saved film`() { + val film = Film( + id = UUID.randomUUID(), + title = "Начало", + description = "Захватывающий триллер о снах внутри снов", + ) + + val savedFilm = filmRepository.save(film) + + assertNotNull(savedFilm) + assertEquals(film.id, savedFilm.id) + assertEquals(film.title, savedFilm.title) + assertEquals(film.description, savedFilm.description) + } + + @Test + fun `should update existing film`() { + val filmId = UUID.randomUUID() + val originalFilm = Film(filmId, "Начало", "Оригинальное описание") + filmRepository.save(originalFilm) + + val updatedFilm = Film(filmId, "Начало (Обновлено)", "Обновленное описание с дополнительными деталями") + val result = filmRepository.save(updatedFilm) + + assertEquals(filmId, result.id) + assertEquals("Начало (Обновлено)", result.title) + assertEquals("Обновленное описание с дополнительными деталями", result.description) + + val foundFilm = filmRepository.findById(filmId) + assertNotNull(foundFilm) + assertEquals("Начало (Обновлено)", foundFilm?.title) + assertEquals("Обновленное описание с дополнительными деталями", foundFilm?.description) + } + + @Test + fun `should find film by id`() { + val film = Film(UUID.randomUUID(), "Матрица", "Хакер узнает правду о реальности") + filmRepository.save(film) + + val foundFilm = filmRepository.findById(film.id) + + assertNotNull(foundFilm) + assertEquals(film.id, foundFilm?.id) + assertEquals(film.title, foundFilm?.title) + assertEquals(film.description, foundFilm?.description) + } + + @Test + fun `should return null when film not found by id`() { + val nonExistentId = UUID.randomUUID() + + val foundFilm = filmRepository.findById(nonExistentId) + + assertNull(foundFilm) + } + + @Test + fun `should find all films`() { + val film1 = Film(UUID.randomUUID(), "Начало", "Сны внутри снов") + val film2 = Film(UUID.randomUUID(), "Матрица", "Реальность не то, чем кажется") + val film3 = Film(UUID.randomUUID(), "Интерстеллар", "Путешествие сквозь пространство и время") + + filmRepository.save(film1) + filmRepository.save(film2) + filmRepository.save(film3) + + val allFilms = filmRepository.findAll() + + assertEquals(3, allFilms.size) + assertTrue(allFilms.any { it.id == film1.id }) + assertTrue(allFilms.any { it.id == film2.id }) + assertTrue(allFilms.any { it.id == film3.id }) + } + + @Test + fun `should return empty list when no films exist`() { + val allFilms = filmRepository.findAll() + + assertTrue(allFilms.isEmpty()) + } + + @Test + fun `should delete film by id`() { + val film = Film(UUID.randomUUID(), "Начало", "Сны внутри снов") + filmRepository.save(film) + + filmRepository.deleteById(film.id) + + val foundFilm = filmRepository.findById(film.id) + assertNull(foundFilm) + } + + @Test + fun `should not throw exception when deleting non-existent film`() { + val nonExistentId = UUID.randomUUID() + + filmRepository.deleteById(nonExistentId) + } + + @Test + fun `should save film with long description`() { + val longDescription = "А".repeat(1000) + val film = Film(UUID.randomUUID(), "Тестовый фильм", longDescription) + + val savedFilm = filmRepository.save(film) + + assertNotNull(savedFilm) + assertEquals(longDescription, savedFilm.description) + } +} diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt new file mode 100644 index 0000000..e1f0cd9 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt @@ -0,0 +1,159 @@ +package com.project.movienight.adapters.persistence.jdbc + +import com.project.movienight.domain.model.User +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +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 java.util.UUID + +@SpringBootTest +@ActiveProfiles("test") +class UserRepositoryIntegrationTest { + @Autowired + private lateinit var userRepository: UserRepository + + @Autowired + private lateinit var jdbcTemplate: JdbcTemplate + + @BeforeEach + fun setup() { + cleanDatabase() + } + + @AfterEach + fun cleanup() { + cleanDatabase() + } + + private fun cleanDatabase() { + jdbcTemplate.execute("DELETE FROM favorites") + jdbcTemplate.execute("DELETE FROM films") + jdbcTemplate.execute("DELETE FROM users") + } + + @Test + fun `should save new user and return saved user`() { + val user = User( + id = UUID.randomUUID(), + name = "John Doe", + email = "john@example.com", + library = null, + ) + + val savedUser = userRepository.save(user) + + assertNotNull(savedUser) + assertEquals(user.id, savedUser.id) + assertEquals(user.name, savedUser.name) + assertEquals(user.email, savedUser.email) + } + + @Test + fun `should update existing user`() { + // given + val userId = UUID.randomUUID() + val originalUser = User(userId, "John Doe", "john@example.com", null) + userRepository.save(originalUser) + + val updatedUser = User(userId, "Jane Doe", "jane@example.com", null) + val result = userRepository.save(updatedUser) + + assertEquals(userId, result.id) + assertEquals("Jane Doe", result.name) + assertEquals("jane@example.com", result.email) + + val foundUser = userRepository.findById(userId) + assertNotNull(foundUser) + assertEquals("Jane Doe", foundUser?.name) + assertEquals("jane@example.com", foundUser?.email) + } + + @Test + fun `should find user by id`() { + // given + val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null) + userRepository.save(user) + + // when + val foundUser = userRepository.findById(user.id) + + // then + assertNotNull(foundUser) + assertEquals(user.id, foundUser?.id) + assertEquals(user.name, foundUser?.name) + assertEquals(user.email, foundUser?.email) + } + + @Test + fun `should return null when user not found by id`() { + // given + val nonExistentId = UUID.randomUUID() + + // when + val foundUser = userRepository.findById(nonExistentId) + + // then + assertNull(foundUser) + } + + @Test + fun `should find all users`() { + // given + val user1 = User(UUID.randomUUID(), "John Doe", "john@example.com", null) + val user2 = User(UUID.randomUUID(), "Jane Smith", "jane@example.com", null) + val user3 = User(UUID.randomUUID(), "Bob Johnson", "bob@example.com", null) + + userRepository.save(user1) + userRepository.save(user2) + userRepository.save(user3) + + // when + val allUsers = userRepository.findAll() + + // then + assertEquals(3, allUsers.size) + assertTrue(allUsers.any { it.id == user1.id }) + assertTrue(allUsers.any { it.id == user2.id }) + assertTrue(allUsers.any { it.id == user3.id }) + } + + @Test + fun `should return empty list when no users exist`() { + // when + val allUsers = userRepository.findAll() + + // then + assertTrue(allUsers.isEmpty()) + } + + @Test + fun `should delete user by id`() { + // given + val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null) + userRepository.save(user) + + // when + userRepository.deleteById(user.id) + + // then + val foundUser = userRepository.findById(user.id) + assertNull(foundUser) + } + + @Test + fun `should not throw exception when deleting non-existent user`() { + // given + val nonExistentId = UUID.randomUUID() + + // when & then (no exception should be thrown) + userRepository.deleteById(nonExistentId) + } +} diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml new file mode 100644 index 0000000..9aabcb5 --- /dev/null +++ b/src/test/resources/application-test.yaml @@ -0,0 +1,28 @@ +spring: + application: + name: MovieNight-Test + datasource: + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + username: sa + password: + driver-class-name: org.h2.Driver + flyway: + enabled: true + url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + locations: classpath:db/migration + baseline-on-migrate: true + h2: + console: + enabled: false + +services: + user: + blocked-names: + - admin + - root + - system + film: + blocked-patterns: + - censored + - epstein + - python -- 2.54.0 From f927148897879803e6eab42925f45999a5541960 Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 21 Apr 2026 00:21:00 +0300 Subject: [PATCH 2/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=20=D1=8E=D0=BD=D0=B8=D1=82=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=B2=D1=81=D0=B5?= =?UTF-8?q?=D1=85=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 1 + .../services/FilmLibraryServiceTest.kt | 280 ++++++++++++++++++ .../application/services/FilmServiceTest.kt | 181 +++++++++++ .../application/services/UserServiceTest.kt | 155 ++++++++++ 4 files changed, 617 insertions(+) create mode 100644 src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt create mode 100644 src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt create mode 100644 src/test/kotlin/com/project/movienight/application/services/UserServiceTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 18d7d23..847a6f5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.spring.boot.starter.test) testImplementation(libs.kotlin.test.junit5) testImplementation(libs.spring.grpc.test) + testImplementation(libs.mockk) testRuntimeOnly(libs.junit.platform.launcher) } diff --git a/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt new file mode 100644 index 0000000..a5951d4 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt @@ -0,0 +1,280 @@ +package com.project.movienight.application.services + +import com.project.movienight.application.ports.input.AddFilmToLibraryCommand +import com.project.movienight.application.ports.input.CreateFilmLibraryCommand +import com.project.movienight.application.ports.input.GetFilmLibraryQuery +import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand +import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.domain.exception.DomainException +import com.project.movienight.domain.exception.EntityNotFoundException +import com.project.movienight.domain.model.FilmLibrary +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.UUID + +class FilmLibraryServiceTest { + private lateinit var filmLibraryRepository: FilmLibraryRepositoryPort + private lateinit var idGenerator: IdGenerator + private lateinit var filmLibraryService: FilmLibraryService + + @BeforeEach + fun setup() { + filmLibraryRepository = mockk() + idGenerator = mockk() + filmLibraryService = FilmLibraryService(filmLibraryRepository, idGenerator) + } + + @Test + fun `should create new film library when user has no library`() { + val userId = UUID.randomUUID() + val libraryId = UUID.randomUUID() + val filmId = UUID.randomUUID() + val command = CreateFilmLibraryCommand(userId = userId, name = "My Films") + val expectedLibrary = FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = "My Films", + isViewed = false, + ) + + every { filmLibraryRepository.findAll() } returns emptyList() + every { idGenerator.generateId() } returnsMany listOf(libraryId, filmId) + every { filmLibraryRepository.save(match { + it.userId == userId && it.comment == "My Films" && it.isViewed == false + }) } returns expectedLibrary + + val result = filmLibraryService.create(command) + + assertNotNull(result) + assertEquals(libraryId, result.id) + assertEquals(userId, result.userId) + assertEquals(filmId, result.filmId) + assertEquals("My Films", result.comment) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 2) { idGenerator.generateId() } + verify(exactly = 1) { filmLibraryRepository.save(any()) } + } + + @Test + fun `should return existing library when user already has one`() { + val userId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = UUID.randomUUID(), + comment = "Existing Library", + isViewed = false, + ) + val command = CreateFilmLibraryCommand(userId = userId, name = "New Library") + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + + val result = filmLibraryService.create(command) + + assertEquals(existingLibrary, result) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 0) { filmLibraryRepository.save(any()) } + } + + @Test + fun `should add film to new library when user has no library`() { + val userId = UUID.randomUUID() + val filmId = UUID.randomUUID() + val libraryId = UUID.randomUUID() + val command = AddFilmToLibraryCommand(userId = userId, filmId = filmId) + val expectedLibrary = FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = null, + isViewed = false, + ) + + every { filmLibraryRepository.findAll() } returns emptyList() + every { idGenerator.generateId() } returns libraryId + every { filmLibraryRepository.save(match { + it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false + }) } returns expectedLibrary + + val result = filmLibraryService.addFilm(command) + + assertNotNull(result) + assertEquals(filmId, result.filmId) + assertEquals(userId, result.userId) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 1) { idGenerator.generateId() } + verify(exactly = 1) { filmLibraryRepository.save(any()) } + } + + @Test + fun `should add film to existing library`() { + val userId = UUID.randomUUID() + val oldFilmId = UUID.randomUUID() + val newFilmId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = oldFilmId, + comment = "My Library", + isViewed = true, + ) + val command = AddFilmToLibraryCommand(userId = userId, filmId = newFilmId) + val updatedLibrary = existingLibrary.copy(filmId = newFilmId, isViewed = false) + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + every { filmLibraryRepository.save(match { + it.filmId == newFilmId && it.isViewed == false + }) } returns updatedLibrary + + val result = filmLibraryService.addFilm(command) + + assertEquals(newFilmId, result.filmId) + assertEquals(false, result.isViewed) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 1) { filmLibraryRepository.save(any()) } + } + + @Test + fun `should remove film from library successfully`() { + val userId = UUID.randomUUID() + val filmId = UUID.randomUUID() + val libraryId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = "My Library", + isViewed = false, + ) + val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId) + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + justRun { filmLibraryRepository.deleteById(libraryId) } + + val result = filmLibraryService.removeFilm(command) + + assertEquals(existingLibrary, result) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 1) { filmLibraryRepository.deleteById(libraryId) } + } + + @Test + fun `should throw EntityNotFoundException when removing film from non-existent library`() { + val userId = UUID.randomUUID() + val filmId = UUID.randomUUID() + val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId) + + every { filmLibraryRepository.findAll() } returns emptyList() + + assertThrows { + filmLibraryService.removeFilm(command) + } + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 0) { filmLibraryRepository.deleteById(any()) } + } + + @Test + fun `should throw DomainException when removing film that is not in library`() { + val userId = UUID.randomUUID() + val libraryFilmId = UUID.randomUUID() + val differentFilmId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = libraryFilmId, + comment = "My Library", + isViewed = false, + ) + val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = differentFilmId) + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + + assertThrows { + filmLibraryService.removeFilm(command) + } + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 0) { filmLibraryRepository.deleteById(any()) } + } + + @Test + fun `should throw EntityNotFoundException when libraryId does not match`() { + val userId = UUID.randomUUID() + val filmId = UUID.randomUUID() + val actualLibraryId = UUID.randomUUID() + val wrongLibraryId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = actualLibraryId, + userId = userId, + filmId = filmId, + comment = "My Library", + isViewed = false, + ) + val command = RemoveFilmFromLibraryCommand( + userId = userId, + filmId = filmId, + libraryId = wrongLibraryId, + ) + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + + assertThrows { + filmLibraryService.removeFilm(command) + } + + verify(exactly = 1) { filmLibraryRepository.findAll() } + verify(exactly = 0) { filmLibraryRepository.deleteById(any()) } + } + + @Test + fun `should get library successfully`() { + val userId = UUID.randomUUID() + val existingLibrary = FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = UUID.randomUUID(), + comment = "My Library", + isViewed = false, + ) + val query = GetFilmLibraryQuery(userId = userId) + + every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) + + val result = filmLibraryService.getLibrary(query) + + assertEquals(existingLibrary, result) + + verify(exactly = 1) { filmLibraryRepository.findAll() } + } + + @Test + fun `should throw EntityNotFoundException when getting non-existent library`() { + val userId = UUID.randomUUID() + val query = GetFilmLibraryQuery(userId = userId) + + every { filmLibraryRepository.findAll() } returns emptyList() + + assertThrows { + filmLibraryService.getLibrary(query) + } + + verify(exactly = 1) { filmLibraryRepository.findAll() } + } +} diff --git a/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt new file mode 100644 index 0000000..fcc5613 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/application/services/FilmServiceTest.kt @@ -0,0 +1,181 @@ +package com.project.movienight.application.services + +import com.project.movienight.application.ports.input.CreateFilmCommand +import com.project.movienight.application.ports.input.EditFilmCommand +import com.project.movienight.application.ports.output.FilmRepositoryPort +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.config.FilmServiceProperties +import com.project.movienight.domain.exception.BlockedValueException +import com.project.movienight.domain.exception.EntityNotFoundException +import com.project.movienight.domain.model.Film +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.UUID + +class FilmServiceTest { + private lateinit var filmRepository: FilmRepositoryPort + private lateinit var idGenerator: IdGenerator + private lateinit var filmConfig: FilmServiceProperties + private lateinit var filmService: FilmService + + @BeforeEach + fun setup() { + filmRepository = mockk() + idGenerator = mockk() + filmConfig = mockk() + filmService = FilmService(filmRepository, idGenerator, filmConfig) + } + + @Test + fun `should create film successfully`() { + val command = CreateFilmCommand(title = "Inception", description = "A mind-bending thriller") + val filmId = UUID.randomUUID() + val expectedFilm = Film(id = filmId, title = "Inception", description = "A mind-bending thriller") + + every { filmConfig.isBlocked("Inception") } returns false + every { filmConfig.isBlocked("A mind-bending thriller") } returns false + every { idGenerator.generateId() } returns filmId + every { filmRepository.save(any()) } returns expectedFilm + + val result = filmService.create(command) + + assertNotNull(result) + assertEquals(filmId, result.id) + assertEquals("Inception", result.title) + assertEquals("A mind-bending thriller", result.description) + + verify(exactly = 1) { filmConfig.isBlocked("Inception") } + verify(exactly = 1) { filmConfig.isBlocked("A mind-bending thriller") } + verify(exactly = 1) { idGenerator.generateId() } + verify(exactly = 1) { filmRepository.save(any()) } + } + + @Test + fun `should throw BlockedValueException when creating film with blocked title`() { + val command = CreateFilmCommand(title = "censored", description = "Some description") + + every { filmConfig.isBlocked("censored") } returns true + every { filmConfig.isBlocked("Some description") } returns false + + assertThrows { + filmService.create(command) + } + + verify(exactly = 1) { filmConfig.isBlocked("censored") } + verify(exactly = 0) { filmConfig.isBlocked("Some description") } + verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 0) { filmRepository.save(any()) } + } + + @Test + fun `should throw BlockedValueException when creating film with blocked description`() { + val command = CreateFilmCommand(title = "Good Film", description = "python") + + every { filmConfig.isBlocked("Good Film") } returns false + every { filmConfig.isBlocked("python") } returns true + + assertThrows { + filmService.create(command) + } + + verify(exactly = 1) { filmConfig.isBlocked("Good Film") } + verify(exactly = 1) { filmConfig.isBlocked("python") } + verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 0) { filmRepository.save(any()) } + } + + @Test + fun `should edit film successfully`() { + val filmId = UUID.randomUUID() + val command = EditFilmCommand(title = "Inception 2", description = "The sequel") + val existingFilm = Film(id = filmId, title = "Inception", description = "A mind-bending thriller") + val updatedFilm = Film(id = filmId, title = "Inception 2", description = "The sequel") + + every { filmConfig.isBlocked("Inception 2") } returns false + every { filmConfig.isBlocked("The sequel") } returns false + every { filmRepository.findById(filmId) } returns existingFilm + every { filmRepository.save(any()) } returns updatedFilm + + val result = filmService.edit(filmId, command) + + assertNotNull(result) + assertEquals(filmId, result.id) + assertEquals("Inception 2", result.title) + assertEquals("The sequel", result.description) + + verify(exactly = 1) { filmConfig.isBlocked("Inception 2") } + verify(exactly = 1) { filmConfig.isBlocked("The sequel") } + verify(exactly = 1) { filmRepository.findById(filmId) } + verify(exactly = 1) { filmRepository.save(any()) } + } + + @Test + fun `should throw BlockedValueException when editing film with blocked title`() { + val filmId = UUID.randomUUID() + val command = EditFilmCommand(title = "epstein", description = "Some description") + + every { filmConfig.isBlocked("epstein") } returns true + + assertThrows { + filmService.edit(filmId, command) + } + + verify(exactly = 1) { filmConfig.isBlocked("epstein") } + verify(exactly = 0) { filmRepository.findById(any()) } + verify(exactly = 0) { filmRepository.save(any()) } + } + + @Test + fun `should throw EntityNotFoundException when editing non-existent film`() { + val filmId = UUID.randomUUID() + val command = EditFilmCommand(title = "New Title", description = "New Description") + + every { filmConfig.isBlocked("New Title") } returns false + every { filmConfig.isBlocked("New Description") } returns false + every { filmRepository.findById(filmId) } returns null + + assertThrows { + filmService.edit(filmId, command) + } + + verify(exactly = 1) { filmConfig.isBlocked("New Title") } + verify(exactly = 1) { filmConfig.isBlocked("New Description") } + verify(exactly = 1) { filmRepository.findById(filmId) } + verify(exactly = 0) { filmRepository.save(any()) } + } + + @Test + fun `should delete film successfully`() { + val filmId = UUID.randomUUID() + val existingFilm = Film(id = filmId, title = "Inception", description = "A mind-bending thriller") + + every { filmRepository.findById(filmId) } returns existingFilm + justRun { filmRepository.deleteById(filmId) } + + filmService.delete(filmId) + + verify(exactly = 1) { filmRepository.findById(filmId) } + verify(exactly = 1) { filmRepository.deleteById(filmId) } + } + + @Test + fun `should throw EntityNotFoundException when deleting non-existent film`() { + val filmId = UUID.randomUUID() + + every { filmRepository.findById(filmId) } returns null + + assertThrows { + filmService.delete(filmId) + } + + verify(exactly = 1) { filmRepository.findById(filmId) } + verify(exactly = 0) { filmRepository.deleteById(any()) } + } +} diff --git a/src/test/kotlin/com/project/movienight/application/services/UserServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/UserServiceTest.kt new file mode 100644 index 0000000..c54a909 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/application/services/UserServiceTest.kt @@ -0,0 +1,155 @@ +package com.project.movienight.application.services + +import com.project.movienight.application.ports.input.CreateUserCommand +import com.project.movienight.application.ports.input.EditUserCommand +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.config.UserServiceProperties +import com.project.movienight.domain.exception.BlockedValueException +import com.project.movienight.domain.exception.EntityNotFoundException +import com.project.movienight.domain.model.User +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.util.UUID + +class UserServiceTest { + private lateinit var userRepository: UserRepositoryPort + private lateinit var idGenerator: IdGenerator + private lateinit var userConfig: UserServiceProperties + private lateinit var userService: UserService + + @BeforeEach + fun setup() { + userRepository = mockk() + idGenerator = mockk() + userConfig = mockk() + userService = UserService(userRepository, idGenerator, userConfig) + } + + @Test + fun `should create user successfully`() { + val command = CreateUserCommand(name = "John Doe", email = "john@example.com") + val userId = UUID.randomUUID() + val expectedUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null) + + every { userConfig.isBlocked("John Doe") } returns false + every { idGenerator.generateId() } returns userId + every { userRepository.save(any()) } returns expectedUser + + val result = userService.create(command) + + assertNotNull(result) + assertEquals(userId, result.id) + assertEquals("John Doe", result.name) + assertEquals("john@example.com", result.email) + + verify(exactly = 1) { userConfig.isBlocked("John Doe") } + verify(exactly = 1) { idGenerator.generateId() } + verify(exactly = 1) { userRepository.save(any()) } + } + + @Test + fun `should throw BlockedValueException when creating user with blocked name`() { + val command = CreateUserCommand(name = "admin", email = "admin@example.com") + + every { userConfig.isBlocked("admin") } returns true + + assertThrows { + userService.create(command) + } + + verify(exactly = 1) { userConfig.isBlocked("admin") } + verify(exactly = 0) { idGenerator.generateId() } + verify(exactly = 0) { userRepository.save(any()) } + } + + @Test + fun `should edit user successfully`() { + val userId = UUID.randomUUID() + val command = EditUserCommand(name = "Jane Doe") + val existingUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null) + val updatedUser = User(id = userId, name = "Jane Doe", email = "john@example.com", library = null) + + every { userConfig.isBlocked("Jane Doe") } returns false + every { userRepository.findById(userId) } returns existingUser + every { userRepository.save(any()) } returns updatedUser + + val result = userService.edit(userId, command) + + assertNotNull(result) + assertEquals(userId, result.id) + assertEquals("Jane Doe", result.name) + + verify(exactly = 1) { userConfig.isBlocked("Jane Doe") } + verify(exactly = 1) { userRepository.findById(userId) } + verify(exactly = 1) { userRepository.save(any()) } + } + + @Test + fun `should throw BlockedValueException when editing user with blocked name`() { + val userId = UUID.randomUUID() + val command = EditUserCommand(name = "root") + + every { userConfig.isBlocked("root") } returns true + + assertThrows { + userService.edit(userId, command) + } + + verify(exactly = 1) { userConfig.isBlocked("root") } + verify(exactly = 0) { userRepository.findById(any()) } + verify(exactly = 0) { userRepository.save(any()) } + } + + @Test + fun `should throw EntityNotFoundException when editing non-existent user`() { + val userId = UUID.randomUUID() + val command = EditUserCommand(name = "Jane Doe") + + every { userConfig.isBlocked("Jane Doe") } returns false + every { userRepository.findById(userId) } returns null + + assertThrows { + userService.edit(userId, command) + } + + verify(exactly = 1) { userConfig.isBlocked("Jane Doe") } + verify(exactly = 1) { userRepository.findById(userId) } + verify(exactly = 0) { userRepository.save(any()) } + } + + @Test + fun `should delete user successfully`() { + val userId = UUID.randomUUID() + val existingUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null) + + every { userRepository.findById(userId) } returns existingUser + justRun { userRepository.deleteById(userId) } + + userService.delete(userId) + + verify(exactly = 1) { userRepository.findById(userId) } + verify(exactly = 1) { userRepository.deleteById(userId) } + } + + @Test + fun `should throw EntityNotFoundException when deleting non-existent user`() { + val userId = UUID.randomUUID() + + every { userRepository.findById(userId) } returns null + + assertThrows { + userService.delete(userId) + } + + verify(exactly = 1) { userRepository.findById(userId) } + verify(exactly = 0) { userRepository.deleteById(any()) } + } +} -- 2.54.0 From 3cb52c019f23d24f8f177c2869d232dd165caaf4 Mon Sep 17 00:00:00 2001 From: skettiks Date: Tue, 21 Apr 2026 15:30:58 +0300 Subject: [PATCH 3/7] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20V2=5F=5Fadd=5Foauth2=5Ffields.sql=20=D0=92=D1=81=D0=B5=20Use?= =?UTF-8?q?rRepositoryIntegrationTest=20=D1=82=D0=B5=D1=81=D1=82=D1=8B=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=88=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ .../resources/db/migration/V2__add_oauth2_fields.sql | 9 +++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/main/resources/db/migration/V2__add_oauth2_fields.sql diff --git a/build.gradle.kts b/build.gradle.kts index 18d7d23..0adabc9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(libs.spring.boot.starter.web) implementation(libs.spring.boot.starter.actuator) -// implementation(libs.spring.boot.starter.security) + implementation(libs.spring.boot.starter.security) implementation(libs.spring.boot.starter.cache) implementation(libs.spring.boot.starter.data.jdbc) implementation(libs.spring.boot.starter.validation) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e91484..e43dfa6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ spring-grpc = "1.0.1" protoc = "3.25.1" grpc-java = "1.60.0" springdoc = "2.8.6" +mockk = "1.13.13" [libraries] spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } @@ -33,6 +34,7 @@ flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry" } sentry-spring-boot-starter = { module = "io.sentry:sentry-spring-boot-starter-jakarta" } diff --git a/src/main/resources/db/migration/V2__add_oauth2_fields.sql b/src/main/resources/db/migration/V2__add_oauth2_fields.sql new file mode 100644 index 0000000..0db4084 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_oauth2_fields.sql @@ -0,0 +1,9 @@ +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); + -- 2.54.0 From 4f0e8ccbd40250d0e5cca4ade8fc2d44c9e7bc03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:28:03 +0000 Subject: [PATCH 4/7] test: fix ktlint multiline wrapping in JDBC integration tests Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/e7455e93-ed32-49f5-9b29-dabcdcc01e87 Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../jdbc/FilmLibraryRepositoryIntegrationTest.kt | 15 ++++++++------- .../jdbc/FilmRepositoryIntegrationTest.kt | 11 ++++++----- .../jdbc/UserRepositoryIntegrationTest.kt | 13 +++++++------ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt index 232216a..d880f02 100644 --- a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt @@ -61,13 +61,14 @@ class FilmLibraryRepositoryIntegrationTest { @Test fun `should save new film library entry and return saved entry`() { - val entry = FilmLibrary( - id = UUID.randomUUID(), - userId = testUser.id, - filmId = testFilm.id, - comment = "Отличный фильм!", - isViewed = false, - ) + val entry = + FilmLibrary( + id = UUID.randomUUID(), + userId = testUser.id, + filmId = testFilm.id, + comment = "Отличный фильм!", + isViewed = false, + ) val savedEntry = filmLibraryRepository.save(entry) diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt index 926177b..3c38a23 100644 --- a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt @@ -41,11 +41,12 @@ class FilmRepositoryIntegrationTest { @Test fun `should save new film and return saved film`() { - val film = Film( - id = UUID.randomUUID(), - title = "Начало", - description = "Захватывающий триллер о снах внутри снов", - ) + val film = + Film( + id = UUID.randomUUID(), + title = "Начало", + description = "Захватывающий триллер о снах внутри снов", + ) val savedFilm = filmRepository.save(film) diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt index e1f0cd9..02b588a 100644 --- a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt @@ -41,12 +41,13 @@ class UserRepositoryIntegrationTest { @Test fun `should save new user and return saved user`() { - val user = User( - id = UUID.randomUUID(), - name = "John Doe", - email = "john@example.com", - library = null, - ) + val user = + User( + id = UUID.randomUUID(), + name = "John Doe", + email = "john@example.com", + library = null, + ) val savedUser = userRepository.save(user) -- 2.54.0 From c64fc35353fec000c6b2264f789cceef79801b59 Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Wed, 22 Apr 2026 08:46:58 +0300 Subject: [PATCH 5/7] =?UTF-8?q?OAuth2=20(=D0=B1=D0=B5=D0=B7=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B2=20=D0=B8=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=81=20java)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 2 + gradle/libs.versions.toml | 1 + .../persistence/jdbc/UserRepository.kt | 10 ++++ .../security/CustomOAuth2UserService.kt | 57 +++++++++++++++++++ .../adapters/security/GoogleOAuth2UserInfo.kt | 16 ++++++ .../adapters/security/OAuth2UserInfo.kt | 9 +++ .../security/OAuth2UserInfoFactory.kt | 18 ++++++ .../adapters/security/UserPrincipal.kt | 42 ++++++++++++++ .../adapters/security/VkOAuth2UserInfo.kt | 26 +++++++++ .../adapters/security/YandexOAuth2UserInfo.kt | 20 +++++++ .../ports/output/UserRepositoryPort.kt | 2 + .../application/services/UserService.kt | 1 + .../project/movienight/domain/model/User.kt | 1 + 13 files changed, 205 insertions(+) create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt create mode 100644 src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt diff --git a/build.gradle.kts b/build.gradle.kts index 18d7d23..609f72b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -47,6 +47,8 @@ dependencies { implementation(libs.spring.grpc.starter) implementation(libs.grpc.services) + implementation("org.springframework.boot:spring-boot-starter-oauth2-client:3.4.3") + runtimeOnly(libs.micrometer.registry.prometheus) runtimeOnly(libs.h2) runtimeOnly(libs.postgresql) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e91484..28cf8d6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ grpc-java = "1.60.0" springdoc = "2.8.6" [libraries] +spring-boot-starter-oauth2-client = { module = "org.springframework.boot:spring-boot-starter-oauth2-client" } spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } spring-boot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator" } spring-boot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security" } diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index a6607e5..8eee586 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -16,6 +16,7 @@ class UserRepository( id = UUID.fromString(rs.getString("id")), name = rs.getString("name"), email = rs.getString("email"), + password = rs.getString("password"), library = null, ) } @@ -56,6 +57,15 @@ class UserRepository( return users.firstOrNull() } + override fun findByEmail(email: String): User? { + val users = jdbc.query( + "SELECT id, name, email, password FROM users WHERE email = ?", + userRowMapper, + email, + ) + return users.firstOrNull() + } + override fun findAll(): List = jdbc.query( "SELECT id, name, email FROM users", diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt new file mode 100644 index 0000000..189e927 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -0,0 +1,57 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.application.ports.output.IdGenerator +import com.project.movienight.application.ports.output.UserRepositoryPort +import com.project.movienight.domain.model.User +import org.slf4j.LoggerFactory +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User +import org.springframework.stereotype.Service + +@Service +class CustomOAuth2UserService( + private val userRepository: UserRepositoryPort, + private val idGenerator: IdGenerator, +) : DefaultOAuth2UserService() { + + companion object { + private val log = LoggerFactory.getLogger(CustomOAuth2UserService::class.java) + } + + override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User { + val oAuth2User = super.loadUser(userRequest) + val registrationId = userRequest.clientRegistration.registrationId + + log.debug("Processing OAuth2 login for provider: {}", registrationId) + + return try { + val userInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(registrationId, oAuth2User) + val user = findOrCreateUser(userInfo) + UserPrincipal.create(user, oAuth2User.attributes) + } catch (e: Exception) { + log.error("OAuth2 authentication failed: ${e.message}", e) + throw OAuth2AuthenticationException("Failed to process OAuth2 user data") + } + } + + private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { + val existingUser = userRepository.findByEmail(userInfo.getEmail()) + + return if (existingUser != null) { + log.debug("User found by email: {}", userInfo.getEmail()) + existingUser + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", // OAuth2 пользователи не имеют пароля + library = null, + ) + userRepository.save(newUser) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt new file mode 100644 index 0000000..dcff86e --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -0,0 +1,16 @@ +package com.project.movienight.adapters.security.oauth2 + +class GoogleOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["sub"] as String + + override fun getEmail(): String = attributes["email"] as String + + override fun getName(): String = attributes["name"] as String + + override fun getProvider(): String = "google" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt new file mode 100644 index 0000000..5a155b9 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -0,0 +1,9 @@ +package com.project.movienight.adapters.security.oauth2 + +interface OAuth2UserInfo { + fun getProviderId(): String + fun getEmail(): String + fun getName(): String + fun getProvider(): String + fun getAttributes(): Map +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt new file mode 100644 index 0000000..001b789 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -0,0 +1,18 @@ +package com.project.movienight.adapters.security.oauth2 + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException +import org.springframework.security.oauth2.core.user.OAuth2User + +object OAuth2UserInfoFactory { + + fun getOAuth2UserInfo(registrationId: String, user: OAuth2User): OAuth2UserInfo { + val attributes = user.attributes + + return when (registrationId.lowercase()) { + "google" -> GoogleOAuth2UserInfo(attributes) + "yandex" -> YandexOAuth2UserInfo(attributes) + "vk" -> VkOAuth2UserInfo(attributes) + else -> throw OAuth2AuthenticationException("Unknown provider: $registrationId") + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt new file mode 100644 index 0000000..5fbbee1 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -0,0 +1,42 @@ +package com.project.movienight.adapters.security.oauth2 + +import com.project.movienight.domain.model.User +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.oauth2.core.user.OAuth2User +import java.util.* + +class UserPrincipal( + private val user: User, + private val attributes: Map? = null, +) : OAuth2User, UserDetails { + + fun getId(): UUID = user.id + + override fun getName(): String = user.name + + override fun getAttributes(): Map = attributes ?: emptyMap() + + override fun getAuthorities(): Collection { + return listOf(SimpleGrantedAuthority("ROLE_USER")) + } + + override fun getPassword(): String = user.password + + override fun getUsername(): String = user.email + + override fun isAccountNonExpired(): Boolean = true + + override fun isAccountNonLocked(): Boolean = true + + override fun isCredentialsNonExpired(): Boolean = true + + override fun isEnabled(): Boolean = true + + companion object { + fun create(user: User, attributes: Map? = null): UserPrincipal { + return UserPrincipal(user, attributes) + } + } +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt new file mode 100644 index 0000000..492e74c --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -0,0 +1,26 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class VkOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String { + val response = attributes["response"] as? List> + return response?.firstOrNull()?.get("id")?.toString() ?: "" + } + + override fun getEmail(): String = attributes["email"] as? String ?: "" + + override fun getName(): String { + val response = attributes["response"] as? List> + val first = response?.firstOrNull() + val firstName = first?.get("first_name") as? String ?: "" + val lastName = first?.get("last_name") as? String ?: "" + return "$firstName $lastName".trim() + } + + override fun getProvider(): String = "vk" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt new file mode 100644 index 0000000..2929389 --- /dev/null +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -0,0 +1,20 @@ +package com.project.movienight.adapters.security.oauth2 + +@Suppress("UNCHECKED_CAST") +class YandexOAuth2UserInfo( + private val attributes: Map +) : OAuth2UserInfo { + + override fun getProviderId(): String = attributes["id"]?.toString() ?: "" + + override fun getEmail(): String { + val emails = attributes["emails"] as? List> + return emails?.firstOrNull()?.get("value") ?: "" + } + + override fun getName(): String = attributes["display_name"] as? String ?: "" + + override fun getProvider(): String = "yandex" + + override fun getAttributes(): Map = attributes +} diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index af8ef0a..cfefe9b 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -8,6 +8,8 @@ interface UserRepositoryPort { fun findById(id: UUID): User? + fun findByEmail(email: String): User? + fun findAll(): List fun deleteById(id: UUID) diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index be32d6a..5eb754e 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -32,6 +32,7 @@ class UserService( id = idGenerator.generateId(), name = command.name, email = command.email, + password = "", library = null, ) return userRepository.save(user) diff --git a/src/main/kotlin/com/project/movienight/domain/model/User.kt b/src/main/kotlin/com/project/movienight/domain/model/User.kt index b4f2d9b..db9142b 100644 --- a/src/main/kotlin/com/project/movienight/domain/model/User.kt +++ b/src/main/kotlin/com/project/movienight/domain/model/User.kt @@ -6,5 +6,6 @@ data class User( val id: UUID, val name: String, val email: String, + val password: String, val library: FilmLibrary?, ) -- 2.54.0 From bd7ecfcab5dc57f3e8ca11a688ef1f998093a728 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:36:27 +0000 Subject: [PATCH 6/7] fix(test): add mockk catalog dependency and format service tests Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/abb7242b-7ff5-4cde-9d34-5733a7f1172f Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- gradle/libs.versions.toml | 2 + .../services/FilmLibraryServiceTest.kt | 161 ++++++++++-------- 2 files changed, 93 insertions(+), 70 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2e91484..7314bb3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ spring-grpc = "1.0.1" protoc = "3.25.1" grpc-java = "1.60.0" springdoc = "2.8.6" +mockk = "1.13.12" [libraries] spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web" } @@ -33,6 +34,7 @@ flyway-database-postgresql = { module = "org.flywaydb:flyway-database-postgresql kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5" } junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } sentry-bom = { module = "io.sentry:sentry-bom", version.ref = "sentry" } sentry-spring-boot-starter = { module = "io.sentry:sentry-spring-boot-starter-jakarta" } diff --git a/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt index a5951d4..1556f83 100644 --- a/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt +++ b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt @@ -38,19 +38,24 @@ class FilmLibraryServiceTest { val libraryId = UUID.randomUUID() val filmId = UUID.randomUUID() val command = CreateFilmLibraryCommand(userId = userId, name = "My Films") - val expectedLibrary = FilmLibrary( - id = libraryId, - userId = userId, - filmId = filmId, - comment = "My Films", - isViewed = false, - ) + val expectedLibrary = + FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = "My Films", + isViewed = false, + ) every { filmLibraryRepository.findAll() } returns emptyList() every { idGenerator.generateId() } returnsMany listOf(libraryId, filmId) - every { filmLibraryRepository.save(match { - it.userId == userId && it.comment == "My Films" && it.isViewed == false - }) } returns expectedLibrary + every { + filmLibraryRepository.save( + match { + it.userId == userId && it.comment == "My Films" && it.isViewed == false + }, + ) + } returns expectedLibrary val result = filmLibraryService.create(command) @@ -68,13 +73,14 @@ class FilmLibraryServiceTest { @Test fun `should return existing library when user already has one`() { val userId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = UUID.randomUUID(), - userId = userId, - filmId = UUID.randomUUID(), - comment = "Existing Library", - isViewed = false, - ) + val existingLibrary = + FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = UUID.randomUUID(), + comment = "Existing Library", + isViewed = false, + ) val command = CreateFilmLibraryCommand(userId = userId, name = "New Library") every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) @@ -94,19 +100,24 @@ class FilmLibraryServiceTest { val filmId = UUID.randomUUID() val libraryId = UUID.randomUUID() val command = AddFilmToLibraryCommand(userId = userId, filmId = filmId) - val expectedLibrary = FilmLibrary( - id = libraryId, - userId = userId, - filmId = filmId, - comment = null, - isViewed = false, - ) + val expectedLibrary = + FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = null, + isViewed = false, + ) every { filmLibraryRepository.findAll() } returns emptyList() every { idGenerator.generateId() } returns libraryId - every { filmLibraryRepository.save(match { - it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false - }) } returns expectedLibrary + every { + filmLibraryRepository.save( + match { + it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false + }, + ) + } returns expectedLibrary val result = filmLibraryService.addFilm(command) @@ -124,20 +135,25 @@ class FilmLibraryServiceTest { val userId = UUID.randomUUID() val oldFilmId = UUID.randomUUID() val newFilmId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = UUID.randomUUID(), - userId = userId, - filmId = oldFilmId, - comment = "My Library", - isViewed = true, - ) + val existingLibrary = + FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = oldFilmId, + comment = "My Library", + isViewed = true, + ) val command = AddFilmToLibraryCommand(userId = userId, filmId = newFilmId) val updatedLibrary = existingLibrary.copy(filmId = newFilmId, isViewed = false) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) - every { filmLibraryRepository.save(match { - it.filmId == newFilmId && it.isViewed == false - }) } returns updatedLibrary + every { + filmLibraryRepository.save( + match { + it.filmId == newFilmId && it.isViewed == false + }, + ) + } returns updatedLibrary val result = filmLibraryService.addFilm(command) @@ -154,13 +170,14 @@ class FilmLibraryServiceTest { val userId = UUID.randomUUID() val filmId = UUID.randomUUID() val libraryId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = libraryId, - userId = userId, - filmId = filmId, - comment = "My Library", - isViewed = false, - ) + val existingLibrary = + FilmLibrary( + id = libraryId, + userId = userId, + filmId = filmId, + comment = "My Library", + isViewed = false, + ) val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) @@ -195,13 +212,14 @@ class FilmLibraryServiceTest { val userId = UUID.randomUUID() val libraryFilmId = UUID.randomUUID() val differentFilmId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = UUID.randomUUID(), - userId = userId, - filmId = libraryFilmId, - comment = "My Library", - isViewed = false, - ) + val existingLibrary = + FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = libraryFilmId, + comment = "My Library", + isViewed = false, + ) val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = differentFilmId) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) @@ -220,18 +238,20 @@ class FilmLibraryServiceTest { val filmId = UUID.randomUUID() val actualLibraryId = UUID.randomUUID() val wrongLibraryId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = actualLibraryId, - userId = userId, - filmId = filmId, - comment = "My Library", - isViewed = false, - ) - val command = RemoveFilmFromLibraryCommand( - userId = userId, - filmId = filmId, - libraryId = wrongLibraryId, - ) + val existingLibrary = + FilmLibrary( + id = actualLibraryId, + userId = userId, + filmId = filmId, + comment = "My Library", + isViewed = false, + ) + val command = + RemoveFilmFromLibraryCommand( + userId = userId, + filmId = filmId, + libraryId = wrongLibraryId, + ) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) @@ -246,13 +266,14 @@ class FilmLibraryServiceTest { @Test fun `should get library successfully`() { val userId = UUID.randomUUID() - val existingLibrary = FilmLibrary( - id = UUID.randomUUID(), - userId = userId, - filmId = UUID.randomUUID(), - comment = "My Library", - isViewed = false, - ) + val existingLibrary = + FilmLibrary( + id = UUID.randomUUID(), + userId = userId, + filmId = UUID.randomUUID(), + comment = "My Library", + isViewed = false, + ) val query = GetFilmLibraryQuery(userId = userId) every { filmLibraryRepository.findAll() } returns listOf(existingLibrary) -- 2.54.0 From d50e2e640cf7ee48c8771d41b5144dd298a76900 Mon Sep 17 00:00:00 2001 From: skettiks Date: Thu, 23 Apr 2026 18:47:30 +0300 Subject: [PATCH 7/7] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20V1=20=D0=BC=D0=B8=D0=B3=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20-=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=BF=D1=8F=D1=82=D0=B0=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20email=20=D0=A0=D0=B5=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20saveWithOAuth2()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=20findByProviderAndProviderId()?= =?UTF-8?q?=20=D0=B2=20UserRepository=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20CustomOAuth2UserService=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B8=D1=81=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20OAuth2=20=D0=BC=D0=B5=D1=82=D0=BE?= =?UTF-8?q?=D0=B4=D0=BE=D0=B2=20=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20package=20declaration=20=D0=B2=D0=BE=20=D0=B2=D1=81?= =?UTF-8?q?=D0=B5=D1=85=207=20OAuth2=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= =?UTF-8?q?=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jdbc/UserRepository.kt | 52 +++++++++++++++++-- .../security/CustomOAuth2UserService.kt | 37 ++++++++----- .../adapters/security/GoogleOAuth2UserInfo.kt | 2 +- .../adapters/security/OAuth2UserInfo.kt | 2 +- .../security/OAuth2UserInfoFactory.kt | 2 +- .../adapters/security/UserPrincipal.kt | 2 +- .../adapters/security/VkOAuth2UserInfo.kt | 2 +- .../adapters/security/YandexOAuth2UserInfo.kt | 2 +- .../ports/output/UserRepositoryPort.kt | 4 ++ src/main/resources/db/migration/V1__init.sql | 3 +- 10 files changed, 84 insertions(+), 24 deletions(-) diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 8eee586..420dbd0 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -26,22 +26,24 @@ class UserRepository( jdbc.update( """ UPDATE users - SET name = ?, email = ? + SET name = ?, email = ?, password = ? WHERE id = ? """.trimIndent(), user.name, user.email, + user.password, user.id, ) if (updatedRows == 0) { jdbc.update( """ - INSERT INTO users (id, name, email) - VALUES (?, ?, ?) + INSERT INTO users (id, name, email, password) + VALUES (?, ?, ?, ?) """.trimIndent(), user.id, user.name, user.email, + user.password, ) } return user @@ -50,7 +52,7 @@ class UserRepository( override fun findById(id: UUID): User? { val users = jdbc.query( - "SELECT id, name, email FROM users WHERE id = ?", + "SELECT id, name, email, password FROM users WHERE id = ?", userRowMapper, id, ) @@ -68,11 +70,51 @@ class UserRepository( override fun findAll(): List = jdbc.query( - "SELECT id, name, email FROM users", + "SELECT id, name, email, password FROM users", userRowMapper, ) override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) } + + override fun saveWithOAuth2(user: User, provider: String, providerId: String): User { + val updatedRows = jdbc.update(""" + UPDATE users + SET name = ?, email = ?, password = ?, provider = ?, provider_id = ? + WHERE id = ? + """.trimIndent(), + 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) + VALUES (?, ?, ?, ?, ?, ?) + """.trimIndent(), + user.id, + user.name, + user.email, + user.password, + provider, + providerId, + ) + } + 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() + } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt index 189e927..968424a 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/CustomOAuth2UserService.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.UserRepositoryPort @@ -37,21 +37,34 @@ class CustomOAuth2UserService( } private fun findOrCreateUser(userInfo: OAuth2UserInfo): User { - val existingUser = userRepository.findByEmail(userInfo.getEmail()) + // Сначала ищем по provider + provider_id (основной способ для OAuth2) + val existingUser = userRepository.findByProviderAndProviderId( + userInfo.getProvider(), + userInfo.getProviderId() + ) return if (existingUser != null) { - log.debug("User found by email: {}", userInfo.getEmail()) + log.debug("User found by provider: {}", userInfo.getProvider()) existingUser } else { - log.debug("Creating new user for provider: {}", userInfo.getProvider()) - val newUser = User( - id = idGenerator.generateId(), - name = userInfo.getName(), - email = userInfo.getEmail(), - password = "", // OAuth2 пользователи не имеют пароля - library = null, - ) - userRepository.save(newUser) + // Проверяем нет ли пользователя с таким email (связывание аккаунтов) + val userByEmail = userRepository.findByEmail(userInfo.getEmail()) + + if (userByEmail != null) { + // Пользователь существует, обновляем его OAuth2 данными + log.debug("Linking OAuth2 account to existing user: {}", userInfo.getEmail()) + userRepository.saveWithOAuth2(userByEmail, userInfo.getProvider(), userInfo.getProviderId()) + } else { + log.debug("Creating new user for provider: {}", userInfo.getProvider()) + val newUser = User( + id = idGenerator.generateId(), + name = userInfo.getName(), + email = userInfo.getEmail(), + password = "", + library = null, + ) + userRepository.saveWithOAuth2(newUser, userInfo.getProvider(), userInfo.getProviderId()) + } } } } diff --git a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt index dcff86e..fa41de5 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/GoogleOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security class GoogleOAuth2UserInfo( private val attributes: Map diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt index 5a155b9..b6abf09 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security interface OAuth2UserInfo { fun getProviderId(): String diff --git a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt index 001b789..e2db545 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/OAuth2UserInfoFactory.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import org.springframework.security.oauth2.core.OAuth2AuthenticationException import org.springframework.security.oauth2.core.user.OAuth2User diff --git a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt index 5fbbee1..a4d94ea 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/UserPrincipal.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security import com.project.movienight.domain.model.User import org.springframework.security.core.GrantedAuthority diff --git a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt index 492e74c..47c41d2 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/VkOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class VkOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt index 2929389..467aa85 100644 --- a/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt +++ b/src/main/kotlin/com/project/movienight/adapters/security/YandexOAuth2UserInfo.kt @@ -1,4 +1,4 @@ -package com.project.movienight.adapters.security.oauth2 +package com.project.movienight.adapters.security @Suppress("UNCHECKED_CAST") class YandexOAuth2UserInfo( diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt index cfefe9b..0e45c1e 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt @@ -6,6 +6,10 @@ import java.util.UUID interface UserRepositoryPort { 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 findByEmail(email: String): User? diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 11017d1..a94ddaf 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,7 +1,8 @@ CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, - email VARCHAR(320) NOT NULL UNIQUE + email VARCHAR(320) NOT NULL UNIQUE, + password VARCHAR(255) ); CREATE TABLE IF NOT EXISTS public.films ( -- 2.54.0