refactor: обновить use case слои и интеграцию Jellyfin
Основные изменения: укрупнены use case-интерфейсы, контроллеры переведены на цельные зависимости, логика доступных фильмов перенесена в FilmLibraryService, добавлены проверки существования фильма и улучшена обработка ошибок API. Метрики: FilmService больше не зависит напрямую от Micrometer для counters, используется BusinessMetricsPort; добавлены TimedAspect и duration-метрики через @Timed для create/edit/delete фильмов. Jellyfin: event handling переведен на транзакционную модель, sync учитывает runtime-ошибки, добавлены plugin-token/web-url настройки, webhook и sync endpoints защищены X-MovieNight-Plugin-Token. Плагин: добавлен Jellyfin plugin в plugins/jellyfin, backend принимает push-sync payload, sync-state доступен плагину, pull-sync вынесен в /api/integrations/jellyfin/pull-sync, README описывает фактический контракт. API и DTO: добавлены RecommendationResponse, ContentTypeParser, validation annotations, обработка validation errors и ResponseStatusException, endpoint /api/users/me. БД: добавлена V7 cleanup-миграция для legacy ratings/jellyfin_id объектов; V4/V5 в этот коммит не включались. Проверка: .\gradlew.bat check --stacktrace проходит полностью.
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package com.project.movienight
|
||||
|
||||
import org.junit.jupiter.api.Test
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class ClassLoaderTest {
|
||||
@Test
|
||||
fun `can load OAuth2ClientProperties class`() {
|
||||
val clazz =
|
||||
Class.forName(
|
||||
"org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties",
|
||||
)
|
||||
assertNotNull(clazz)
|
||||
println("Successfully loaded: ${clazz.name}")
|
||||
println("ClassLoader: ${clazz.classLoader}")
|
||||
}
|
||||
}
|
||||
-4
@@ -26,7 +26,6 @@ class UserEntityMappingTest {
|
||||
assertEquals(entity.id, user.id)
|
||||
assertEquals(entity.name, user.name)
|
||||
assertEquals(entity.email, user.email)
|
||||
assertNull(user.library)
|
||||
assertNull(user.jellyfinUserId)
|
||||
}
|
||||
|
||||
@@ -37,7 +36,6 @@ class UserEntityMappingTest {
|
||||
id = UUID.randomUUID(),
|
||||
name = "Jane",
|
||||
email = "jane@mail.com",
|
||||
library = null,
|
||||
)
|
||||
|
||||
val entity = user.toEntity(AuthProvider.YANDEX, "yandex456")
|
||||
@@ -56,7 +54,6 @@ class UserEntityMappingTest {
|
||||
id = UUID.randomUUID(),
|
||||
name = "Bob",
|
||||
email = "bob@mail.com",
|
||||
library = null,
|
||||
)
|
||||
|
||||
val entity = user.toEntity()
|
||||
@@ -72,7 +69,6 @@ class UserEntityMappingTest {
|
||||
id = UUID.randomUUID(),
|
||||
name = "Alice",
|
||||
email = "alice@email.com",
|
||||
library = null,
|
||||
)
|
||||
|
||||
val mapped = original.toEntity().toDomain()
|
||||
|
||||
+39
-39
@@ -1,7 +1,7 @@
|
||||
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.FilmLibraryEntry
|
||||
import com.project.movienight.domain.model.User
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -19,9 +19,9 @@ import java.util.UUID
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class FilmLibraryRepositoryIntegrationTest {
|
||||
class FilmLibraryEntryRepositoryIntegrationTest {
|
||||
@Autowired
|
||||
private lateinit var filmLibraryRepository: FilmLibraryRepository
|
||||
private lateinit var filmLibraryEntryRepository: FilmLibraryEntryRepository
|
||||
|
||||
@Autowired
|
||||
private lateinit var userRepository: UserRepository
|
||||
@@ -62,7 +62,7 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
@Test
|
||||
fun `should save new film library entry and return saved entry`() {
|
||||
val entry =
|
||||
FilmLibrary(
|
||||
FilmLibraryEntry(
|
||||
id = UUID.randomUUID(),
|
||||
userId = testUser.id,
|
||||
filmId = testFilm.id,
|
||||
@@ -70,7 +70,7 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
val savedEntry = filmLibraryRepository.save(entry)
|
||||
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||
|
||||
assertNotNull(savedEntry)
|
||||
assertEquals(entry.id, savedEntry.id)
|
||||
@@ -83,17 +83,17 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
@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 originalEntry = FilmLibraryEntry(entryId, testUser.id, testFilm.id, "Хочу посмотреть", false)
|
||||
filmLibraryEntryRepository.save(originalEntry)
|
||||
|
||||
val updatedEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true)
|
||||
val result = filmLibraryRepository.save(updatedEntry)
|
||||
val updatedEntry = FilmLibraryEntry(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true)
|
||||
val result = filmLibraryEntryRepository.save(updatedEntry)
|
||||
|
||||
assertEquals(entryId, result.id)
|
||||
assertEquals("Уже посмотрел, потрясающе!", result.comment)
|
||||
assertTrue(result.isViewed)
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(entryId)
|
||||
val foundEntry = filmLibraryEntryRepository.findById(entryId)
|
||||
assertNotNull(foundEntry)
|
||||
assertEquals("Уже посмотрел, потрясающе!", foundEntry?.comment)
|
||||
assertTrue(foundEntry?.isViewed ?: false)
|
||||
@@ -101,10 +101,10 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `should find film library entry by id`() {
|
||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false)
|
||||
filmLibraryRepository.save(entry)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false)
|
||||
filmLibraryEntryRepository.save(entry)
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
||||
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||
|
||||
assertNotNull(foundEntry)
|
||||
assertEquals(entry.id, foundEntry?.id)
|
||||
@@ -118,22 +118,22 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
fun `should return null when film library entry not found by id`() {
|
||||
val nonExistentId = UUID.randomUUID()
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(nonExistentId)
|
||||
val foundEntry = filmLibraryEntryRepository.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)
|
||||
val entry1 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 1", false)
|
||||
val entry2 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 2", true)
|
||||
val entry3 = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
||||
|
||||
filmLibraryRepository.save(entry1)
|
||||
filmLibraryRepository.save(entry2)
|
||||
filmLibraryRepository.save(entry3)
|
||||
filmLibraryEntryRepository.save(entry1)
|
||||
filmLibraryEntryRepository.save(entry2)
|
||||
filmLibraryEntryRepository.save(entry3)
|
||||
|
||||
val allEntries = filmLibraryRepository.findAll()
|
||||
val allEntries = filmLibraryEntryRepository.findAll()
|
||||
|
||||
assertEquals(3, allEntries.size)
|
||||
assertTrue(allEntries.any { it.id == entry1.id })
|
||||
@@ -143,19 +143,19 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `should return empty list when no film library entries exist`() {
|
||||
val allEntries = filmLibraryRepository.findAll()
|
||||
val allEntries = filmLibraryEntryRepository.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)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Для удаления", false)
|
||||
filmLibraryEntryRepository.save(entry)
|
||||
|
||||
filmLibraryRepository.deleteById(entry.id)
|
||||
filmLibraryEntryRepository.deleteById(entry.id)
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
||||
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||
assertNull(foundEntry)
|
||||
}
|
||||
|
||||
@@ -163,14 +163,14 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
fun `should not throw exception when deleting non-existent entry`() {
|
||||
val nonExistentId = UUID.randomUUID()
|
||||
|
||||
filmLibraryRepository.deleteById(nonExistentId)
|
||||
filmLibraryEntryRepository.deleteById(nonExistentId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should save entry with null comment`() {
|
||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
|
||||
|
||||
val savedEntry = filmLibraryRepository.save(entry)
|
||||
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||
|
||||
assertNotNull(savedEntry)
|
||||
assertNull(savedEntry.comment)
|
||||
@@ -178,9 +178,9 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `should save entry with isViewed true`() {
|
||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true)
|
||||
|
||||
val savedEntry = filmLibraryRepository.save(entry)
|
||||
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||
|
||||
assertNotNull(savedEntry)
|
||||
assertTrue(savedEntry.isViewed)
|
||||
@@ -188,9 +188,9 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `should save entry with isViewed false`() {
|
||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false)
|
||||
|
||||
val savedEntry = filmLibraryRepository.save(entry)
|
||||
val savedEntry = filmLibraryEntryRepository.save(entry)
|
||||
|
||||
assertNotNull(savedEntry)
|
||||
assertFalse(savedEntry.isViewed)
|
||||
@@ -198,23 +198,23 @@ class FilmLibraryRepositoryIntegrationTest {
|
||||
|
||||
@Test
|
||||
fun `should cascade delete entries when user is deleted`() {
|
||||
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false)
|
||||
filmLibraryRepository.save(entry)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false)
|
||||
filmLibraryEntryRepository.save(entry)
|
||||
|
||||
userRepository.deleteById(testUser.id)
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
||||
val foundEntry = filmLibraryEntryRepository.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)
|
||||
val entry = FilmLibraryEntry(UUID.randomUUID(), testUser.id, testFilm.id, "Запись о фильме", false)
|
||||
filmLibraryEntryRepository.save(entry)
|
||||
|
||||
filmRepository.deleteById(testFilm.id)
|
||||
|
||||
val foundEntry = filmLibraryRepository.findById(entry.id)
|
||||
val foundEntry = filmLibraryEntryRepository.findById(entry.id)
|
||||
assertNull(foundEntry)
|
||||
}
|
||||
}
|
||||
+45
-8
@@ -1,5 +1,6 @@
|
||||
package com.project.movienight.adapters.persistence.jdbc
|
||||
|
||||
import com.project.movienight.domain.model.AuthProvider
|
||||
import com.project.movienight.domain.model.User
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -46,7 +47,6 @@ class UserRepositoryIntegrationTest {
|
||||
id = UUID.randomUUID(),
|
||||
name = "John Doe",
|
||||
email = "john@example.com",
|
||||
library = null,
|
||||
)
|
||||
|
||||
val savedUser = userRepository.save(user)
|
||||
@@ -61,10 +61,10 @@ class UserRepositoryIntegrationTest {
|
||||
fun `should update existing user`() {
|
||||
// given
|
||||
val userId = UUID.randomUUID()
|
||||
val originalUser = User(userId, "John Doe", "john@example.com", null)
|
||||
val originalUser = User(userId, "John Doe", "john@example.com")
|
||||
userRepository.save(originalUser)
|
||||
|
||||
val updatedUser = User(userId, "Jane Doe", "jane@example.com", null)
|
||||
val updatedUser = User(userId, "Jane Doe", "jane@example.com")
|
||||
val result = userRepository.save(updatedUser)
|
||||
|
||||
assertEquals(userId, result.id)
|
||||
@@ -80,7 +80,7 @@ class UserRepositoryIntegrationTest {
|
||||
@Test
|
||||
fun `should find user by id`() {
|
||||
// given
|
||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
|
||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||
userRepository.save(user)
|
||||
|
||||
// when
|
||||
@@ -108,9 +108,9 @@ class UserRepositoryIntegrationTest {
|
||||
@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)
|
||||
val user1 = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||
val user2 = User(UUID.randomUUID(), "Jane Smith", "jane@example.com")
|
||||
val user3 = User(UUID.randomUUID(), "Bob Johnson", "bob@example.com")
|
||||
|
||||
userRepository.save(user1)
|
||||
userRepository.save(user2)
|
||||
@@ -138,7 +138,7 @@ class UserRepositoryIntegrationTest {
|
||||
@Test
|
||||
fun `should delete user by id`() {
|
||||
// given
|
||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
|
||||
val user = User(UUID.randomUUID(), "John Doe", "john@example.com")
|
||||
userRepository.save(user)
|
||||
|
||||
// when
|
||||
@@ -157,4 +157,41 @@ class UserRepositoryIntegrationTest {
|
||||
// when & then (no exception should be thrown)
|
||||
userRepository.deleteById(nonExistentId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should create OAuth user with provider identity`() {
|
||||
val user = User(UUID.randomUUID(), "OAuth User", "oauth@example.com")
|
||||
|
||||
val savedUser = userRepository.createOAuthUser(user, AuthProvider.GOOGLE, "google-123")
|
||||
|
||||
assertEquals(user.id, savedUser.id)
|
||||
assertEquals(user.email, savedUser.email)
|
||||
|
||||
val foundByProvider = userRepository.findByProviderAndProviderId(AuthProvider.GOOGLE, "google-123")
|
||||
assertNotNull(foundByProvider)
|
||||
assertEquals(user.id, foundByProvider?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should link OAuth account to existing user`() {
|
||||
val user = userRepository.save(User(UUID.randomUUID(), "Link User", "link@example.com"))
|
||||
|
||||
val linkedUser = userRepository.linkOAuthAccount(user.id, AuthProvider.YANDEX, "yandex-456")
|
||||
|
||||
assertEquals(user.id, linkedUser.id)
|
||||
val foundByProvider = userRepository.findByProviderAndProviderId(AuthProvider.YANDEX, "yandex-456")
|
||||
assertNotNull(foundByProvider)
|
||||
assertEquals(user.id, foundByProvider?.id)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `find by email should include jellyfin user id`() {
|
||||
val user = userRepository.save(User(UUID.randomUUID(), "Jellyfin User", "jellyfin@example.com"))
|
||||
jdbcTemplate.update("UPDATE users SET jellyfin_user_id = ? WHERE id = ?", "jellyfin-789", user.id)
|
||||
|
||||
val foundUser = userRepository.findByEmail(user.email)
|
||||
|
||||
assertNotNull(foundUser)
|
||||
assertEquals("jellyfin-789", foundUser?.jellyfinUserId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package com.project.movienight.adapters.web
|
||||
|
||||
import com.project.movienight.application.ports.input.CreateFilmUseCase
|
||||
import com.project.movienight.application.ports.input.DeleteFilmUseCase
|
||||
import com.project.movienight.application.ports.input.EditFilmUseCase
|
||||
import com.project.movienight.application.ports.input.GetAllFilmsUseCase
|
||||
import com.project.movienight.application.ports.input.GetFilmByIdUseCase
|
||||
import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase
|
||||
import com.project.movienight.application.ports.input.FilmUseCase
|
||||
import com.project.movienight.domain.model.Film
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
@@ -19,20 +14,15 @@ import java.util.UUID
|
||||
|
||||
class FilmControllerSearchTest {
|
||||
private lateinit var mockMvc: MockMvc
|
||||
private lateinit var searchFilmByTitleUseCase: SearchFilmByTitleUseCase
|
||||
private lateinit var filmUseCase: FilmUseCase
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
searchFilmByTitleUseCase = mockk()
|
||||
filmUseCase = mockk()
|
||||
|
||||
val controller =
|
||||
FilmController(
|
||||
createFilmUseCase = mockk<CreateFilmUseCase>(),
|
||||
editFilmUseCase = mockk<EditFilmUseCase>(),
|
||||
deleteFilmUseCase = mockk<DeleteFilmUseCase>(),
|
||||
getFilmByIdUseCase = mockk<GetFilmByIdUseCase>(),
|
||||
getAllFilmsUseCase = mockk<GetAllFilmsUseCase>(),
|
||||
searchFilmByTitleUseCase = searchFilmByTitleUseCase,
|
||||
filmUseCase = filmUseCase,
|
||||
)
|
||||
|
||||
mockMvc = MockMvcBuilders.standaloneSetup(controller).build()
|
||||
@@ -43,7 +33,7 @@ class FilmControllerSearchTest {
|
||||
val title = "Inception"
|
||||
val film = Film(id = UUID.randomUUID(), title = title, description = "A dream heist")
|
||||
|
||||
every { searchFilmByTitleUseCase.searchByTitle(title) } returns film
|
||||
every { filmUseCase.searchByTitle(title) } returns film
|
||||
|
||||
mockMvc
|
||||
.get("/api/films/search") {
|
||||
@@ -55,14 +45,14 @@ class FilmControllerSearchTest {
|
||||
jsonPath("$.description") { value("A dream heist") }
|
||||
}
|
||||
|
||||
verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) }
|
||||
verify(exactly = 1) { filmUseCase.searchByTitle(title) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `search returns 404 when title is not found`() {
|
||||
val title = "Unknown Title"
|
||||
|
||||
every { searchFilmByTitleUseCase.searchByTitle(title) } returns null
|
||||
every { filmUseCase.searchByTitle(title) } returns null
|
||||
|
||||
mockMvc
|
||||
.get("/api/films/search") {
|
||||
@@ -72,6 +62,6 @@ class FilmControllerSearchTest {
|
||||
content { string("") }
|
||||
}
|
||||
|
||||
verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) }
|
||||
verify(exactly = 1) { filmUseCase.searchByTitle(title) }
|
||||
}
|
||||
}
|
||||
|
||||
+75
-174
@@ -1,15 +1,15 @@
|
||||
package com.project.movienight.application.services
|
||||
|
||||
import com.project.movienight.adapters.metrics.BusinessMetricsService
|
||||
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.BusinessMetricsPort
|
||||
import com.project.movienight.application.ports.output.FilmLibraryEntryRepositoryPort
|
||||
import com.project.movienight.application.ports.output.FilmRepositoryPort
|
||||
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 com.project.movienight.domain.model.Film
|
||||
import com.project.movienight.domain.model.FilmLibraryEntry
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
@@ -22,83 +22,52 @@ import org.junit.jupiter.api.assertThrows
|
||||
import java.util.UUID
|
||||
|
||||
class FilmLibraryServiceTest {
|
||||
private lateinit var filmLibraryRepository: FilmLibraryRepositoryPort
|
||||
private lateinit var filmLibraryEntryRepository: FilmLibraryEntryRepositoryPort
|
||||
private lateinit var filmRepository: FilmRepositoryPort
|
||||
private lateinit var idGenerator: IdGenerator
|
||||
private lateinit var businessMetricsService: BusinessMetricsService
|
||||
private lateinit var businessMetricsService: BusinessMetricsPort
|
||||
private lateinit var filmLibraryService: FilmLibraryService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
filmLibraryRepository = mockk()
|
||||
filmLibraryEntryRepository = mockk()
|
||||
filmRepository = mockk()
|
||||
idGenerator = mockk()
|
||||
businessMetricsService = mockk(relaxed = true)
|
||||
filmLibraryService = FilmLibraryService(filmLibraryRepository, idGenerator, businessMetricsService)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw EntityNotFoundException when creating library for user with no entries`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val command = CreateFilmLibraryCommand(userId = userId, name = "My Films")
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||
|
||||
assertThrows<EntityNotFoundException> {
|
||||
filmLibraryService.create(command)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 0) { idGenerator.generateId() }
|
||||
verify(exactly = 0) { 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,
|
||||
filmLibraryService =
|
||||
FilmLibraryService(
|
||||
filmLibraryEntryRepository,
|
||||
filmRepository,
|
||||
idGenerator,
|
||||
businessMetricsService,
|
||||
)
|
||||
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`() {
|
||||
fun `should add film as new library entry`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val filmId = UUID.randomUUID()
|
||||
val libraryId = UUID.randomUUID()
|
||||
val entryId = UUID.randomUUID()
|
||||
val command = AddFilmToLibraryCommand(userId = userId, filmId = filmId)
|
||||
val expectedLibrary =
|
||||
FilmLibrary(
|
||||
id = libraryId,
|
||||
val expectedEntry =
|
||||
FilmLibraryEntry(
|
||||
id = entryId,
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
comment = null,
|
||||
isViewed = false,
|
||||
)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||
every { idGenerator.generateId() } returns libraryId
|
||||
every { filmRepository.findById(filmId) } returns Film(filmId, "Film", "Description")
|
||||
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns null
|
||||
every { idGenerator.generateId() } returns entryId
|
||||
every {
|
||||
filmLibraryRepository.save(
|
||||
filmLibraryEntryRepository.save(
|
||||
match {
|
||||
it.userId == userId && it.filmId == filmId && it.comment == null && it.isViewed == false
|
||||
},
|
||||
)
|
||||
} returns expectedLibrary
|
||||
} returns expectedEntry
|
||||
|
||||
val result = filmLibraryService.addFilm(command)
|
||||
|
||||
@@ -106,62 +75,46 @@ class FilmLibraryServiceTest {
|
||||
assertEquals(filmId, result.filmId)
|
||||
assertEquals(userId, result.userId)
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||
verify(exactly = 1) { idGenerator.generateId() }
|
||||
verify(exactly = 1) { filmLibraryRepository.save(any()) }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.save(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should add film as a new library entry when another film already exists`() {
|
||||
fun `should reset viewed state when adding existing entry`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val oldFilmId = UUID.randomUUID()
|
||||
val newFilmId = UUID.randomUUID()
|
||||
val existingLibrary =
|
||||
FilmLibrary(
|
||||
val filmId = UUID.randomUUID()
|
||||
val existingEntry =
|
||||
FilmLibraryEntry(
|
||||
id = UUID.randomUUID(),
|
||||
userId = userId,
|
||||
filmId = oldFilmId,
|
||||
filmId = filmId,
|
||||
comment = "My Library",
|
||||
isViewed = true,
|
||||
)
|
||||
val command = AddFilmToLibraryCommand(userId = userId, filmId = newFilmId)
|
||||
val createdLibrary =
|
||||
FilmLibrary(
|
||||
id = UUID.randomUUID(),
|
||||
userId = userId,
|
||||
filmId = newFilmId,
|
||||
comment = null,
|
||||
isViewed = false,
|
||||
)
|
||||
val updatedEntry = existingEntry.copy(isViewed = false, watchedAt = null)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
||||
every { idGenerator.generateId() } returns createdLibrary.id
|
||||
every {
|
||||
filmLibraryRepository.save(
|
||||
match {
|
||||
it.id == createdLibrary.id && it.userId == userId && it.filmId == newFilmId && it.isViewed == false
|
||||
},
|
||||
)
|
||||
} returns createdLibrary
|
||||
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns existingEntry
|
||||
every { filmRepository.findById(filmId) } returns Film(filmId, "Film", "Description")
|
||||
every { filmLibraryEntryRepository.save(updatedEntry) } returns updatedEntry
|
||||
|
||||
val result = filmLibraryService.addFilm(command)
|
||||
val result = filmLibraryService.addFilm(AddFilmToLibraryCommand(userId = userId, filmId = filmId))
|
||||
|
||||
assertEquals(newFilmId, result.filmId)
|
||||
assertEquals(false, result.isViewed)
|
||||
assertEquals(updatedEntry, result)
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 1) { idGenerator.generateId() }
|
||||
verify(exactly = 1) { filmLibraryRepository.save(any()) }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||
verify(exactly = 0) { idGenerator.generateId() }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.save(updatedEntry) }
|
||||
}
|
||||
|
||||
@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,
|
||||
val entryId = UUID.randomUUID()
|
||||
val existingEntry =
|
||||
FilmLibraryEntry(
|
||||
id = entryId,
|
||||
userId = userId,
|
||||
filmId = filmId,
|
||||
comment = "My Library",
|
||||
@@ -169,122 +122,70 @@ class FilmLibraryServiceTest {
|
||||
)
|
||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
||||
justRun { filmLibraryRepository.deleteById(libraryId) }
|
||||
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns existingEntry
|
||||
justRun { filmLibraryEntryRepository.deleteById(entryId) }
|
||||
|
||||
val result = filmLibraryService.removeFilm(command)
|
||||
|
||||
assertEquals(existingLibrary, result)
|
||||
assertEquals(existingEntry, result)
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 1) { filmLibraryRepository.deleteById(libraryId) }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.deleteById(entryId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw EntityNotFoundException when removing film from non-existent library`() {
|
||||
fun `should throw EntityNotFoundException when removing non-existent entry`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val filmId = UUID.randomUUID()
|
||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||
every { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) } returns null
|
||||
|
||||
assertThrows<EntityNotFoundException> {
|
||||
filmLibraryService.removeFilm(command)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findByUserIdAndFilmId(userId, filmId) }
|
||||
verify(exactly = 0) { filmLibraryEntryRepository.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<DomainException> {
|
||||
filmLibraryService.removeFilm(command)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw EntityNotFoundException when libraryId does not match`() {
|
||||
fun `should throw DomainException when entry id belongs to another film`() {
|
||||
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.findById(wrongLibraryId) } returns null
|
||||
|
||||
assertThrows<EntityNotFoundException> {
|
||||
filmLibraryService.removeFilm(command)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findById(wrongLibraryId) }
|
||||
verify(exactly = 0) { filmLibraryRepository.deleteById(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should get library successfully`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val existingLibrary =
|
||||
FilmLibrary(
|
||||
id = UUID.randomUUID(),
|
||||
val entryId = UUID.randomUUID()
|
||||
val existingEntry =
|
||||
FilmLibraryEntry(
|
||||
id = entryId,
|
||||
userId = userId,
|
||||
filmId = UUID.randomUUID(),
|
||||
comment = "My Library",
|
||||
isViewed = false,
|
||||
)
|
||||
val query = GetFilmLibraryQuery(userId = userId)
|
||||
val command = RemoveFilmFromLibraryCommand(userId = userId, filmId = filmId, entryId = entryId)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns listOf(existingLibrary)
|
||||
every { filmLibraryEntryRepository.findById(entryId) } returns existingEntry
|
||||
|
||||
val result = filmLibraryService.getLibrary(query)
|
||||
assertThrows<DomainException> {
|
||||
filmLibraryService.removeFilm(command)
|
||||
}
|
||||
|
||||
assertEquals(existingLibrary, result)
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findById(entryId) }
|
||||
verify(exactly = 0) { filmLibraryEntryRepository.deleteById(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should throw EntityNotFoundException when getting non-existent library`() {
|
||||
fun `should list entries by user`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val query = GetFilmLibraryQuery(userId = userId)
|
||||
val entries =
|
||||
listOf(
|
||||
FilmLibraryEntry(UUID.randomUUID(), userId, UUID.randomUUID(), null, false),
|
||||
)
|
||||
|
||||
every { filmLibraryRepository.findAll() } returns emptyList()
|
||||
every { filmLibraryEntryRepository.findByUserId(userId) } returns entries
|
||||
|
||||
assertThrows<EntityNotFoundException> {
|
||||
filmLibraryService.getLibrary(query)
|
||||
}
|
||||
assertEquals(entries, filmLibraryService.list(userId))
|
||||
|
||||
verify(exactly = 1) { filmLibraryRepository.findAll() }
|
||||
verify(exactly = 1) { filmLibraryEntryRepository.findByUserId(userId) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@ 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.BusinessMetricsPort
|
||||
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.micrometer.core.instrument.simple.SimpleMeterRegistry
|
||||
import io.mockk.every
|
||||
import io.mockk.justRun
|
||||
import io.mockk.mockk
|
||||
@@ -24,7 +24,7 @@ class FilmServiceTest {
|
||||
private lateinit var filmRepository: FilmRepositoryPort
|
||||
private lateinit var idGenerator: IdGenerator
|
||||
private lateinit var filmConfig: FilmServiceProperties
|
||||
private lateinit var meterRegistry: SimpleMeterRegistry
|
||||
private lateinit var businessMetricsService: BusinessMetricsPort
|
||||
private lateinit var filmService: FilmService
|
||||
|
||||
@BeforeEach
|
||||
@@ -32,8 +32,8 @@ class FilmServiceTest {
|
||||
filmRepository = mockk()
|
||||
idGenerator = mockk()
|
||||
filmConfig = mockk()
|
||||
meterRegistry = SimpleMeterRegistry()
|
||||
filmService = FilmService(filmRepository, idGenerator, filmConfig, meterRegistry)
|
||||
businessMetricsService = mockk(relaxed = true)
|
||||
filmService = FilmService(filmRepository, idGenerator, filmConfig, businessMetricsService)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -37,7 +37,7 @@ class UserServiceTest {
|
||||
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)
|
||||
val expectedUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||
|
||||
every { userConfig.isBlocked("John Doe") } returns false
|
||||
every { idGenerator.generateId() } returns userId
|
||||
@@ -74,8 +74,8 @@ class UserServiceTest {
|
||||
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)
|
||||
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||
val updatedUser = User(id = userId, name = "Jane Doe", email = "john@example.com")
|
||||
|
||||
every { userConfig.isBlocked("Jane Doe") } returns false
|
||||
every { userRepository.findById(userId) } returns existingUser
|
||||
@@ -128,7 +128,7 @@ class UserServiceTest {
|
||||
@Test
|
||||
fun `should delete user successfully`() {
|
||||
val userId = UUID.randomUUID()
|
||||
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com", library = null)
|
||||
val existingUser = User(id = userId, name = "John Doe", email = "john@example.com")
|
||||
|
||||
every { userRepository.findById(userId) } returns existingUser
|
||||
justRun { userRepository.deleteById(userId) }
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.project.movienight.config
|
||||
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Primary
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@TestConfiguration
|
||||
@EnableWebSecurity
|
||||
class TestSecurityConfiguration {
|
||||
@Bean
|
||||
@Primary
|
||||
fun testSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
.authorizeHttpRequests { auth ->
|
||||
auth.anyRequest().permitAll()
|
||||
}.csrf { csrf ->
|
||||
csrf.disable()
|
||||
}.headers { headers ->
|
||||
headers.frameOptions { frameOptions ->
|
||||
frameOptions.sameOrigin()
|
||||
}
|
||||
}
|
||||
|
||||
return http.build()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user