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:
skettiks
2026-05-22 12:32:16 +03:00
parent f4bccf4bf0
commit 43be102d4e
86 changed files with 2920 additions and 1004 deletions
@@ -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}")
}
}
@@ -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()
@@ -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)
}
}
@@ -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) }
}
}
@@ -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()
}
}