Merge branch 'develop' into feat/spring-tests

This commit is contained in:
glashark
2026-05-03 20:08:31 +03:00
committed by GitHub
27 changed files with 1715 additions and 58 deletions
+54
View File
@@ -0,0 +1,54 @@
name: Build & Test
on:
workflow_call:
outputs:
artifact-name:
description: "Uploaded artifact name for downstream jobs"
value: ${{ jobs.build.outputs.artifact-name }}
jobs:
build:
name: Build & Test
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
outputs:
artifact-name: ${{ steps.meta.outputs.artifact-name }}
steps:
- name: Checkout source
uses: actions/checkout@v6
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "21"
cache: gradle
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
- name: Run CI quality gate
run: ./gradlew clean check bootJar --stacktrace --no-daemon
- name: Set artifact name
id: meta
run: echo "artifact-name=build-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_OUTPUT"
- name: Upload build artifacts
if: always()
uses: actions/upload-artifact@v7
with:
name: ${{ steps.meta.outputs.artifact-name }}
path: |
build/libs/*.jar
build/reports/detekt/**
build/reports/ktlint/**
build/reports/jacoco/**
build/reports/**
build/test-results/**
build/jacoco/**
retention-days: 7
if-no-files-found: error
+86
View File
@@ -0,0 +1,86 @@
name: CI
run-name: "${{ github.ref_name }} - ${{ github.event_name }} by @${{ github.actor }}"
on:
push:
branches: [develop, main]
tags: ["v*"]
pull_request:
branches: [develop, main]
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
trufflehog:
name: TruffleHog Secret Scan
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout source
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Run TruffleHog
uses: trufflesecurity/trufflehog@main
with:
extra_args: --results=verified,unknown
build:
name: Build & Test
needs: [trufflehog]
uses: ./.github/workflows/build.yaml
docker:
name: Docker
needs: build
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
uses: ./.github/workflows/docker.yaml
permissions:
contents: read
packages: write
with:
push: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
secrets: inherit
notify-main:
name: Notify Main Build
needs: docker
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Send Telegram notification
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
DIGEST: ${{ needs.docker.outputs.image-digest }}
run: |
COMMIT_URL="https://github.com/${REPO}/commit/${SHA}"
MSG="*${REPO}* - main branch CI succeeded"
MSG="${MSG}%0A🔗 [Commit](${COMMIT_URL})"
if [ -n "${DIGEST}" ]; then
MSG="${MSG}%0AImage: \`ghcr.io/${REPO}@${DIGEST}\`"
fi
curl -sf -X POST \
"https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=Markdown" \
-d "text=${MSG}"
release:
name: Release
needs: docker
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
uses: ./.github/workflows/release.yaml
with:
version: ${{ github.ref_name }}
image-digest: ${{ needs.docker.outputs.image-digest }}
secrets: inherit
+77
View File
@@ -0,0 +1,77 @@
name: Docker Build & Push
on:
workflow_call:
inputs:
push:
description: "Push image to GHCR"
type: boolean
required: true
outputs:
image-digest:
description: "Pushed image digest (sha256:…)"
value: ${{ jobs.docker.outputs.image-digest }}
image-tags:
description: "Comma-separated list of applied tags"
value: ${{ jobs.docker.outputs.image-tags }}
jobs:
docker:
name: Docker Build & Push
runs-on: ubuntu-latest
timeout-minutes: 20
outputs:
image-digest: ${{ steps.build-push.outputs.digest }}
image-tags: ${{ steps.meta.outputs.tags }}
steps:
- name: Checkout source
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build image (no push)
if: inputs.push == false
uses: docker/build-push-action@v7
with:
context: .
file: ./Containerfile
push: false
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Log in to GHCR
if: inputs.push == true
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
if: inputs.push == true
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-
- name: Build and push image
if: inputs.push == true
id: build-push
uses: docker/build-push-action@v7
with:
context: .
file: ./Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
+58
View File
@@ -0,0 +1,58 @@
name: Release & Notify
on:
workflow_call:
inputs:
version:
description: "Tag name, e.g. v1.2.3"
type: string
required: true
image-digest:
description: "Docker image digest from docker job"
type: string
required: false
default: ""
secrets:
TELEGRAM_BOT_TOKEN:
required: true
TELEGRAM_CHAT_ID:
required: true
jobs:
release:
name: GitHub Release
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
if gh release view "${{ inputs.version }}" >/dev/null 2>&1; then
echo "Release ${{ inputs.version }} already exists. Skipping creation."
else
gh release create "${{ inputs.version }}" \
--generate-notes \
--title "${{ inputs.version }}"
fi
- name: Send Telegram notification
env:
TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
VERSION: ${{ inputs.version }}
REPO: ${{ github.repository }}
DIGEST: ${{ inputs.image-digest }}
run: |
RELEASE_URL="https://github.com/${REPO}/releases/tag/${VERSION}"
MSG="*${REPO}* - released *${VERSION}*"
MSG="${MSG}%0A🔗 [Release notes](${RELEASE_URL})"
if [ -n "${DIGEST}" ]; then
MSG="${MSG}%0AImage: \`ghcr.io/${REPO}@${DIGEST}\`"
fi
curl -sf -X POST \
"https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d "chat_id=${CHAT_ID}" \
-d "parse_mode=Markdown" \
-d "text=${MSG}"
+3
View File
@@ -42,6 +42,9 @@ gradle-app.setting
*.tar.gz
*.rar
# Gradle wrapper
!gradle/wrapper/gradle-wrapper.jar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
+1 -1
View File
@@ -2,6 +2,6 @@
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.1.20" />
<option name="version" value="2.0.21" />
</component>
</project>
+3 -8
View File
@@ -26,13 +26,7 @@ RUN --mount=type=cache,target=${GRADLE_USER_HOME} \
COPY src src
RUN --mount=type=cache,target=${GRADLE_USER_HOME} \
./gradlew --no-daemon build \
-x test \
-x detekt \
-x ktlintCheck \
-x ktlintKotlinScriptCheck \
-x ktlintMainSourceSetCheck \
-x ktlintTestSourceSetCheck
./gradlew --no-daemon bootJar
RUN mkdir -p ${APP_HOME}/dist \
&& cp ${APP_HOME}/build/libs/*.jar ${APP_HOME}/dist/${JAR_NAME} \
@@ -60,6 +54,7 @@ COPY --from=builder --chown=app:app /workspace/dist/app.jar /app/app.jar
USER app
ENV JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC" \
SPRING_AOT_ENABLED=true \
SERVER_PORT=8080 \
PATH="/app:$PATH"
@@ -70,4 +65,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \
STOPSIGNAL SIGTERM
ENTRYPOINT ["sh", "-c", "exec java ${JAVA_OPTS} -Dserver.port=${SERVER_PORT} -jar /app/app.jar"]
ENTRYPOINT ["sh", "-c", "exec java ${JAVA_OPTS} -Dspring.aot.enabled=${SPRING_AOT_ENABLED} -Dserver.port=${SERVER_PORT} -jar /app/app.jar"]
+3
View File
@@ -15,6 +15,8 @@ plugins {
jacoco
}
apply(plugin = "org.springframework.boot.aot")
apply(from = "$rootDir/gradle/docker.gradle.kts")
group = "com.project"
@@ -56,6 +58,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)
}
+2
View File
@@ -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" }
Binary file not shown.
@@ -0,0 +1,37 @@
package com.project.movienight.adapters.persistence.entity
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User
import java.time.LocalDateTime
import java.util.UUID
data class UserEntity(
val id: UUID,
val name: String,
val email: String,
val provider: String?,
val providerId: String?,
val createdAt: LocalDateTime,
)
fun UserEntity.toDomain(): User =
User(
id = id,
name = name,
email = email,
library = null,
)
fun User.toEntity(
provider: AuthProvider? = null,
providerId: String? = null,
createdAt: LocalDateTime = LocalDateTime.now(),
): UserEntity =
UserEntity(
id = id,
name = name,
email = email,
provider = provider?.name,
providerId = providerId,
createdAt = createdAt,
)
@@ -1,6 +1,10 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.adapters.persistence.entity.UserEntity
import com.project.movienight.adapters.persistence.entity.toDomain
import com.project.movienight.adapters.persistence.entity.toEntity
import com.project.movienight.application.ports.output.UserRepositoryPort
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.stereotype.Repository
@@ -11,58 +15,80 @@ import java.util.UUID
class UserRepository(
private val jdbc: JdbcTemplate,
) : UserRepositoryPort {
private val userRowMapper = { rs: ResultSet, _: Int ->
User(
private val userEntityRowMapper = { rs: ResultSet, _: Int ->
UserEntity(
id = UUID.fromString(rs.getString("id")),
name = rs.getString("name"),
email = rs.getString("email"),
library = null,
provider = rs.getString("provider"),
providerId = rs.getString("provider_id"),
createdAt = rs.getTimestamp("created_at").toLocalDateTime(),
)
}
override fun save(user: User): User {
val entity = user.toEntity()
val updatedRows =
jdbc.update(
"""
UPDATE users
SET name = ?, email = ?
SET name = ?, email = ?, provider = ?, provider_id = ?
WHERE id = ?
""".trimIndent(),
user.name,
user.email,
user.id,
entity.name,
entity.email,
entity.provider,
entity.providerId,
entity.id,
)
if (updatedRows == 0) {
jdbc.update(
"""
INSERT INTO users (id, name, email)
VALUES (?, ?, ?)
INSERT INTO users (id, name, email, provider, provider_id, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""".trimIndent(),
user.id,
user.name,
user.email,
entity.id,
entity.name,
entity.email,
entity.provider,
entity.providerId,
entity.createdAt,
)
}
return user
}
override fun findById(id: UUID): User? {
val users =
val entities =
jdbc.query(
"SELECT id, name, email FROM users WHERE id = ?",
userRowMapper,
"SELECT id, name, email, provider, provider_id, created_at FROM users WHERE id = ?",
userEntityRowMapper,
id,
)
return users.firstOrNull()
return entities.firstOrNull()?.toDomain()
}
override fun findAll(): List<User> =
jdbc.query(
"SELECT id, name, email FROM users",
userRowMapper,
)
"SELECT id, name, email, provider, provider_id, created_at FROM users",
userEntityRowMapper,
).map { it.toDomain() }
override fun deleteById(id: UUID) {
jdbc.update("DELETE FROM users WHERE id = ?", id)
}
override fun findByProviderAndProviderId(
provider: AuthProvider,
providerId: String,
): User? {
val entities =
jdbc.query(
"SELECT id, name, email, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ?",
userEntityRowMapper,
provider.name,
providerId,
)
return entities.firstOrNull()?.toDomain()
}
}
@@ -57,11 +57,10 @@ class FilmController(
FilmResponse.fromDomain(
editFilmUseCase.edit(
id = id,
command =
EditFilmCommand(
title = request.title,
description = request.description,
),
command = EditFilmCommand(
title = request.title,
description = request.description,
),
),
)
@@ -8,8 +8,11 @@ import com.project.movienight.application.ports.input.CreateUserUseCase
import com.project.movienight.application.ports.input.DeleteUserUseCase
import com.project.movienight.application.ports.input.EditUserCommand
import com.project.movienight.application.ports.input.EditUserUseCase
import com.project.movienight.application.ports.input.GetAllUsersUseCase
import com.project.movienight.application.ports.input.GetUserByIdUseCase
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
@@ -25,6 +28,8 @@ class UserController(
private val createUserUseCase: CreateUserUseCase,
private val editUserUseCase: EditUserUseCase,
private val deleteUserUseCase: DeleteUserUseCase,
private val getUserByIdUseCase: GetUserByIdUseCase,
private val getAllUsersUseCase: GetAllUsersUseCase,
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@@ -40,6 +45,16 @@ class UserController(
),
)
@GetMapping
fun getAll(): List<UserResponse> =
getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) }
@GetMapping("/{id}")
fun getById(
@PathVariable id: UUID,
): UserResponse =
UserResponse.fromDomain(getUserByIdUseCase.getById(id))
@PatchMapping("/{id}")
fun edit(
@PathVariable id: UUID,
@@ -48,10 +63,9 @@ class UserController(
UserResponse.fromDomain(
editUserUseCase.edit(
id = id,
command =
EditUserCommand(
name = request.name,
),
command = EditUserCommand(
name = request.name,
),
),
)
@@ -26,3 +26,11 @@ data class EditUserCommand(
interface DeleteUserUseCase {
fun delete(id: UUID)
}
interface GetUserByIdUseCase {
fun getById(id: UUID): User
}
interface GetAllUsersUseCase {
fun getAll(): List<User>
}
@@ -1,5 +1,6 @@
package com.project.movienight.application.ports.output
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User
import java.util.UUID
@@ -11,4 +12,9 @@ interface UserRepositoryPort {
fun findAll(): List<User>
fun deleteById(id: UUID)
fun findByProviderAndProviderId(
provider: AuthProvider,
providerId: String,
): User?
}
@@ -36,19 +36,15 @@ class FilmService(
throw BlockedValueException(target = "Film", field = "description")
}
val film =
Film(
id = idGenerator.generateId(),
title = command.title,
description = command.description,
)
val film = Film(
id = idGenerator.generateId(),
title = command.title,
description = command.description,
)
return filmRepository.save(film)
}
override fun edit(
id: UUID,
command: EditFilmCommand,
): Film {
override fun edit(id: UUID, command: EditFilmCommand): Film {
if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title")
}
@@ -5,6 +5,8 @@ import com.project.movienight.application.ports.input.CreateUserUseCase
import com.project.movienight.application.ports.input.DeleteUserUseCase
import com.project.movienight.application.ports.input.EditUserCommand
import com.project.movienight.application.ports.input.EditUserUseCase
import com.project.movienight.application.ports.input.GetAllUsersUseCase
import com.project.movienight.application.ports.input.GetUserByIdUseCase
import com.project.movienight.application.ports.output.IdGenerator
import com.project.movienight.application.ports.output.UserRepositoryPort
import com.project.movienight.config.UserServiceProperties
@@ -21,19 +23,21 @@ class UserService(
private val userConfig: UserServiceProperties,
) : CreateUserUseCase,
EditUserUseCase,
DeleteUserUseCase {
DeleteUserUseCase,
GetUserByIdUseCase,
GetAllUsersUseCase {
override fun create(command: CreateUserCommand): User {
if (userConfig.isBlocked(command.name)) {
throw BlockedValueException(target = "User", field = "name")
}
val user =
User(
id = idGenerator.generateId(),
name = command.name,
email = command.email,
library = null,
)
val user = User(
id = idGenerator.generateId(),
name = command.name,
email = command.email,
library = null,
)
return userRepository.save(user)
}
@@ -46,15 +50,18 @@ class UserService(
}
var user = userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString())
user = user.copy(name = command.name)
return userRepository.save(user)
}
override fun delete(id: UUID) {
userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString())
userRepository.deleteById(id)
}
override fun getById(id: UUID): User {
return userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString())
}
override fun getAll(): List<User> = userRepository.findAll()
}
@@ -0,0 +1,7 @@
package com.project.movienight.domain.model
enum class AuthProvider {
GOOGLE,
YANDEX,
VK,
}
@@ -0,0 +1,81 @@
package com.project.movienight.adapters.persistence.entity
import com.project.movienight.domain.model.AuthProvider
import com.project.movienight.domain.model.User
import org.junit.jupiter.api.Test
import java.time.LocalDateTime
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertNull
class UserEntityMappingTest {
@Test
fun `toDomain maps UserEntity correctly`() {
val entity = UserEntity(
id = UUID.randomUUID(),
name = "John Pork",
email = "john@email.com",
provider = "GOOGLE",
providerId = "google1234",
createdAt = LocalDateTime.now(),
)
val user = entity.toDomain()
assertEquals(entity.id, user.id)
assertEquals(entity.name, user.name)
assertEquals(entity.email, user.email)
assertNull(user.library)
}
@Test
fun `toEntity maps User with OAuth provider`() {
val user = User(
id = UUID.randomUUID(),
name = "Jane",
email = "jane@mail.com",
library = null
)
val entity = user.toEntity(AuthProvider.YANDEX, "yandex456")
assertEquals(user.id, entity.id)
assertEquals(user.name, entity.name)
assertEquals(user.email, entity.email)
assertEquals("YANDEX", entity.provider)
assertEquals("yandex456", entity.providerId)
}
@Test
fun `toEntity maps User without OAuth provider`() {
val user = User(
id = UUID.randomUUID(),
name = "Bob",
email = "bob@mail.com",
library = null
)
val entity = user.toEntity()
assertNull(entity.provider)
assertNull(entity.providerId)
}
@Test
fun `mapping is reversible for basic fields`() {
val original = User(
id = UUID.randomUUID(),
name = "Alice",
email = "alice@email.com",
library = null
)
val mapped = original.toEntity().toDomain()
assertEquals(original.id, mapped.id)
assertEquals(original.name, mapped.name)
assertEquals(original.email, mapped.email)
}
}
@@ -0,0 +1,220 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.domain.model.Film
import com.project.movienight.domain.model.FilmLibrary
import com.project.movienight.domain.model.User
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import java.util.UUID
@SpringBootTest
@ActiveProfiles("test")
class FilmLibraryRepositoryIntegrationTest {
@Autowired
private lateinit var filmLibraryRepository: FilmLibraryRepository
@Autowired
private lateinit var userRepository: UserRepository
@Autowired
private lateinit var filmRepository: FilmRepository
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
private lateinit var testUser: User
private lateinit var testFilm: Film
@BeforeEach
fun setup() {
cleanDatabase()
createTestData()
}
@AfterEach
fun cleanup() {
cleanDatabase()
}
private fun cleanDatabase() {
jdbcTemplate.execute("DELETE FROM favorites")
jdbcTemplate.execute("DELETE FROM films")
jdbcTemplate.execute("DELETE FROM users")
}
private fun createTestData() {
testUser = User(UUID.randomUUID(), "Иван Иванов", "ivan@example.com", null)
testFilm = Film(UUID.randomUUID(), "Начало", "Захватывающий триллер")
userRepository.save(testUser)
filmRepository.save(testFilm)
}
@Test
fun `should save new film library entry and return saved entry`() {
val entry =
FilmLibrary(
id = UUID.randomUUID(),
userId = testUser.id,
filmId = testFilm.id,
comment = "Отличный фильм!",
isViewed = false,
)
val savedEntry = filmLibraryRepository.save(entry)
assertNotNull(savedEntry)
assertEquals(entry.id, savedEntry.id)
assertEquals(entry.userId, savedEntry.userId)
assertEquals(entry.filmId, savedEntry.filmId)
assertEquals(entry.comment, savedEntry.comment)
assertEquals(entry.isViewed, savedEntry.isViewed)
}
@Test
fun `should update existing film library entry`() {
val entryId = UUID.randomUUID()
val originalEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Хочу посмотреть", false)
filmLibraryRepository.save(originalEntry)
val updatedEntry = FilmLibrary(entryId, testUser.id, testFilm.id, "Уже посмотрел, потрясающе!", true)
val result = filmLibraryRepository.save(updatedEntry)
assertEquals(entryId, result.id)
assertEquals("Уже посмотрел, потрясающе!", result.comment)
assertTrue(result.isViewed)
val foundEntry = filmLibraryRepository.findById(entryId)
assertNotNull(foundEntry)
assertEquals("Уже посмотрел, потрясающе!", foundEntry?.comment)
assertTrue(foundEntry?.isViewed ?: false)
}
@Test
fun `should find film library entry by id`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Обязательно посмотреть", false)
filmLibraryRepository.save(entry)
val foundEntry = filmLibraryRepository.findById(entry.id)
assertNotNull(foundEntry)
assertEquals(entry.id, foundEntry?.id)
assertEquals(entry.userId, foundEntry?.userId)
assertEquals(entry.filmId, foundEntry?.filmId)
assertEquals(entry.comment, foundEntry?.comment)
assertEquals(entry.isViewed, foundEntry?.isViewed)
}
@Test
fun `should return null when film library entry not found by id`() {
val nonExistentId = UUID.randomUUID()
val foundEntry = filmLibraryRepository.findById(nonExistentId)
assertNull(foundEntry)
}
@Test
fun `should find all film library entries`() {
val entry1 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 1", false)
val entry2 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Комментарий 2", true)
val entry3 = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
filmLibraryRepository.save(entry1)
filmLibraryRepository.save(entry2)
filmLibraryRepository.save(entry3)
val allEntries = filmLibraryRepository.findAll()
assertEquals(3, allEntries.size)
assertTrue(allEntries.any { it.id == entry1.id })
assertTrue(allEntries.any { it.id == entry2.id })
assertTrue(allEntries.any { it.id == entry3.id })
}
@Test
fun `should return empty list when no film library entries exist`() {
val allEntries = filmLibraryRepository.findAll()
assertTrue(allEntries.isEmpty())
}
@Test
fun `should delete film library entry by id`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Для удаления", false)
filmLibraryRepository.save(entry)
filmLibraryRepository.deleteById(entry.id)
val foundEntry = filmLibraryRepository.findById(entry.id)
assertNull(foundEntry)
}
@Test
fun `should not throw exception when deleting non-existent entry`() {
val nonExistentId = UUID.randomUUID()
filmLibraryRepository.deleteById(nonExistentId)
}
@Test
fun `should save entry with null comment`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, null, false)
val savedEntry = filmLibraryRepository.save(entry)
assertNotNull(savedEntry)
assertNull(savedEntry.comment)
}
@Test
fun `should save entry with isViewed true`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Посмотрел", true)
val savedEntry = filmLibraryRepository.save(entry)
assertNotNull(savedEntry)
assertTrue(savedEntry.isViewed)
}
@Test
fun `should save entry with isViewed false`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Еще не смотрел", false)
val savedEntry = filmLibraryRepository.save(entry)
assertNotNull(savedEntry)
assertFalse(savedEntry.isViewed)
}
@Test
fun `should cascade delete entries when user is deleted`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Любимый фильм пользователя", false)
filmLibraryRepository.save(entry)
userRepository.deleteById(testUser.id)
val foundEntry = filmLibraryRepository.findById(entry.id)
assertNull(foundEntry)
}
@Test
fun `should cascade delete entries when film is deleted`() {
val entry = FilmLibrary(UUID.randomUUID(), testUser.id, testFilm.id, "Запись о фильме", false)
filmLibraryRepository.save(entry)
filmRepository.deleteById(testFilm.id)
val foundEntry = filmLibraryRepository.findById(entry.id)
assertNull(foundEntry)
}
}
@@ -0,0 +1,153 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.domain.model.Film
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import java.util.UUID
@SpringBootTest
@ActiveProfiles("test")
class FilmRepositoryIntegrationTest {
@Autowired
private lateinit var filmRepository: FilmRepository
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
@BeforeEach
fun setup() {
cleanDatabase()
}
@AfterEach
fun cleanup() {
cleanDatabase()
}
private fun cleanDatabase() {
jdbcTemplate.execute("DELETE FROM favorites")
jdbcTemplate.execute("DELETE FROM films")
jdbcTemplate.execute("DELETE FROM users")
}
@Test
fun `should save new film and return saved film`() {
val film =
Film(
id = UUID.randomUUID(),
title = "Начало",
description = "Захватывающий триллер о снах внутри снов",
)
val savedFilm = filmRepository.save(film)
assertNotNull(savedFilm)
assertEquals(film.id, savedFilm.id)
assertEquals(film.title, savedFilm.title)
assertEquals(film.description, savedFilm.description)
}
@Test
fun `should update existing film`() {
val filmId = UUID.randomUUID()
val originalFilm = Film(filmId, "Начало", "Оригинальное описание")
filmRepository.save(originalFilm)
val updatedFilm = Film(filmId, "Начало (Обновлено)", "Обновленное описание с дополнительными деталями")
val result = filmRepository.save(updatedFilm)
assertEquals(filmId, result.id)
assertEquals("Начало (Обновлено)", result.title)
assertEquals("Обновленное описание с дополнительными деталями", result.description)
val foundFilm = filmRepository.findById(filmId)
assertNotNull(foundFilm)
assertEquals("Начало (Обновлено)", foundFilm?.title)
assertEquals("Обновленное описание с дополнительными деталями", foundFilm?.description)
}
@Test
fun `should find film by id`() {
val film = Film(UUID.randomUUID(), "Матрица", "Хакер узнает правду о реальности")
filmRepository.save(film)
val foundFilm = filmRepository.findById(film.id)
assertNotNull(foundFilm)
assertEquals(film.id, foundFilm?.id)
assertEquals(film.title, foundFilm?.title)
assertEquals(film.description, foundFilm?.description)
}
@Test
fun `should return null when film not found by id`() {
val nonExistentId = UUID.randomUUID()
val foundFilm = filmRepository.findById(nonExistentId)
assertNull(foundFilm)
}
@Test
fun `should find all films`() {
val film1 = Film(UUID.randomUUID(), "Начало", "Сны внутри снов")
val film2 = Film(UUID.randomUUID(), "Матрица", "Реальность не то, чем кажется")
val film3 = Film(UUID.randomUUID(), "Интерстеллар", "Путешествие сквозь пространство и время")
filmRepository.save(film1)
filmRepository.save(film2)
filmRepository.save(film3)
val allFilms = filmRepository.findAll()
assertEquals(3, allFilms.size)
assertTrue(allFilms.any { it.id == film1.id })
assertTrue(allFilms.any { it.id == film2.id })
assertTrue(allFilms.any { it.id == film3.id })
}
@Test
fun `should return empty list when no films exist`() {
val allFilms = filmRepository.findAll()
assertTrue(allFilms.isEmpty())
}
@Test
fun `should delete film by id`() {
val film = Film(UUID.randomUUID(), "Начало", "Сны внутри снов")
filmRepository.save(film)
filmRepository.deleteById(film.id)
val foundFilm = filmRepository.findById(film.id)
assertNull(foundFilm)
}
@Test
fun `should not throw exception when deleting non-existent film`() {
val nonExistentId = UUID.randomUUID()
filmRepository.deleteById(nonExistentId)
}
@Test
fun `should save film with long description`() {
val longDescription = "А".repeat(1000)
val film = Film(UUID.randomUUID(), "Тестовый фильм", longDescription)
val savedFilm = filmRepository.save(film)
assertNotNull(savedFilm)
assertEquals(longDescription, savedFilm.description)
}
}
@@ -0,0 +1,160 @@
package com.project.movienight.adapters.persistence.jdbc
import com.project.movienight.domain.model.User
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import java.util.UUID
@SpringBootTest
@ActiveProfiles("test")
class UserRepositoryIntegrationTest {
@Autowired
private lateinit var userRepository: UserRepository
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
@BeforeEach
fun setup() {
cleanDatabase()
}
@AfterEach
fun cleanup() {
cleanDatabase()
}
private fun cleanDatabase() {
jdbcTemplate.execute("DELETE FROM favorites")
jdbcTemplate.execute("DELETE FROM films")
jdbcTemplate.execute("DELETE FROM users")
}
@Test
fun `should save new user and return saved user`() {
val user =
User(
id = UUID.randomUUID(),
name = "John Doe",
email = "john@example.com",
library = null,
)
val savedUser = userRepository.save(user)
assertNotNull(savedUser)
assertEquals(user.id, savedUser.id)
assertEquals(user.name, savedUser.name)
assertEquals(user.email, savedUser.email)
}
@Test
fun `should update existing user`() {
// given
val userId = UUID.randomUUID()
val originalUser = User(userId, "John Doe", "john@example.com", null)
userRepository.save(originalUser)
val updatedUser = User(userId, "Jane Doe", "jane@example.com", null)
val result = userRepository.save(updatedUser)
assertEquals(userId, result.id)
assertEquals("Jane Doe", result.name)
assertEquals("jane@example.com", result.email)
val foundUser = userRepository.findById(userId)
assertNotNull(foundUser)
assertEquals("Jane Doe", foundUser?.name)
assertEquals("jane@example.com", foundUser?.email)
}
@Test
fun `should find user by id`() {
// given
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
userRepository.save(user)
// when
val foundUser = userRepository.findById(user.id)
// then
assertNotNull(foundUser)
assertEquals(user.id, foundUser?.id)
assertEquals(user.name, foundUser?.name)
assertEquals(user.email, foundUser?.email)
}
@Test
fun `should return null when user not found by id`() {
// given
val nonExistentId = UUID.randomUUID()
// when
val foundUser = userRepository.findById(nonExistentId)
// then
assertNull(foundUser)
}
@Test
fun `should find all users`() {
// given
val user1 = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
val user2 = User(UUID.randomUUID(), "Jane Smith", "jane@example.com", null)
val user3 = User(UUID.randomUUID(), "Bob Johnson", "bob@example.com", null)
userRepository.save(user1)
userRepository.save(user2)
userRepository.save(user3)
// when
val allUsers = userRepository.findAll()
// then
assertEquals(3, allUsers.size)
assertTrue(allUsers.any { it.id == user1.id })
assertTrue(allUsers.any { it.id == user2.id })
assertTrue(allUsers.any { it.id == user3.id })
}
@Test
fun `should return empty list when no users exist`() {
// when
val allUsers = userRepository.findAll()
// then
assertTrue(allUsers.isEmpty())
}
@Test
fun `should delete user by id`() {
// given
val user = User(UUID.randomUUID(), "John Doe", "john@example.com", null)
userRepository.save(user)
// when
userRepository.deleteById(user.id)
// then
val foundUser = userRepository.findById(user.id)
assertNull(foundUser)
}
@Test
fun `should not throw exception when deleting non-existent user`() {
// given
val nonExistentId = UUID.randomUUID()
// when & then (no exception should be thrown)
userRepository.deleteById(nonExistentId)
}
}
@@ -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<EntityNotFoundException> {
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<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`() {
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<EntityNotFoundException> {
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<EntityNotFoundException> {
filmLibraryService.getLibrary(query)
}
verify(exactly = 1) { filmLibraryRepository.findAll() }
}
}
@@ -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<BlockedValueException> {
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<BlockedValueException> {
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<BlockedValueException> {
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<EntityNotFoundException> {
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<EntityNotFoundException> {
filmService.delete(filmId)
}
verify(exactly = 1) { filmRepository.findById(filmId) }
verify(exactly = 0) { filmRepository.deleteById(any()) }
}
}
@@ -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<BlockedValueException> {
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<BlockedValueException> {
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<EntityNotFoundException> {
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<EntityNotFoundException> {
userService.delete(userId)
}
verify(exactly = 1) { userRepository.findById(userId) }
verify(exactly = 0) { userRepository.deleteById(any()) }
}
}
+28
View File
@@ -0,0 +1,28 @@
spring:
application:
name: MovieNight-Test
datasource:
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
driver-class-name: org.h2.Driver
flyway:
enabled: true
url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
locations: classpath:db/migration
baseline-on-migrate: true
h2:
console:
enabled: false
services:
user:
blocked-names:
- admin
- root
- system
film:
blocked-patterns:
- censored
- epstein
- python