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/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 new file mode 100644 index 0000000..1556f83 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/application/services/FilmLibraryServiceTest.kt @@ -0,0 +1,301 @@ +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()) } + } +}