diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
new file mode 100644
index 0000000..8cd8d59
--- /dev/null
+++ b/.github/workflows/build.yaml
@@ -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
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..ece7eb1
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -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
diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml
new file mode 100644
index 0000000..0854d36
--- /dev/null
+++ b/.github/workflows/docker.yaml
@@ -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
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..0fdfcb1
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -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}"
diff --git a/.gitignore b/.gitignore
index ee59915..795dcc9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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*
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
index 88cf87c..9b361e3 100644
--- a/.idea/kotlinc.xml
+++ b/.idea/kotlinc.xml
@@ -2,6 +2,6 @@
-
+
\ No newline at end of file
diff --git a/Containerfile b/Containerfile
index 7e1867b..1fda1fa 100644
--- a/Containerfile
+++ b/Containerfile
@@ -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"]
diff --git a/build.gradle.kts b/build.gradle.kts
index a5b9744..f20ddff 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -15,6 +15,8 @@ plugins {
jacoco
}
+apply(plugin = "org.springframework.boot.aot")
+
apply(from = "$rootDir/gradle/docker.gradle.kts")
group = "com.project"
@@ -57,6 +59,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/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..61285a6
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt
new file mode 100644
index 0000000..0beda74
--- /dev/null
+++ b/src/main/kotlin/com/project/movienight/adapters/persistence/entity/UserEntity.kt
@@ -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,
+ )
diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt
index 420dbd0..cb866d9 100644
--- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt
+++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt
@@ -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,8 +15,8 @@ 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"),
@@ -22,6 +26,7 @@ class UserRepository(
}
override fun save(user: User): User {
+ val entity = user.toEntity()
val updatedRows =
jdbc.update(
"""
@@ -50,13 +55,13 @@ class UserRepository(
}
override fun findById(id: UUID): User? {
- val users =
+ val entities =
jdbc.query(
"SELECT id, name, email, password FROM users WHERE id = ?",
userRowMapper,
id,
)
- return users.firstOrNull()
+ return entities.firstOrNull()?.toDomain()
}
override fun findByEmail(email: String): User? {
diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt
index 0e45c1e..678cea0 100644
--- a/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt
+++ b/src/main/kotlin/com/project/movienight/application/ports/output/UserRepositoryPort.kt
@@ -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
@@ -17,4 +18,9 @@ interface UserRepositoryPort {
fun findAll(): List
fun deleteById(id: UUID)
+
+ fun findByProviderAndProviderId(
+ provider: AuthProvider,
+ providerId: String,
+ ): User?
}
diff --git a/src/main/kotlin/com/project/movienight/domain/model/AuthProvider.kt b/src/main/kotlin/com/project/movienight/domain/model/AuthProvider.kt
new file mode 100644
index 0000000..4926b93
--- /dev/null
+++ b/src/main/kotlin/com/project/movienight/domain/model/AuthProvider.kt
@@ -0,0 +1,7 @@
+package com.project.movienight.domain.model
+
+enum class AuthProvider {
+ GOOGLE,
+ YANDEX,
+ VK,
+}
diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt
new file mode 100644
index 0000000..bec7f20
--- /dev/null
+++ b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt
@@ -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)
+ }
+
+
+}
diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt
new file mode 100644
index 0000000..d880f02
--- /dev/null
+++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmLibraryRepositoryIntegrationTest.kt
@@ -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)
+ }
+}
diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt
new file mode 100644
index 0000000..3c38a23
--- /dev/null
+++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepositoryIntegrationTest.kt
@@ -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)
+ }
+}
diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt
new file mode 100644
index 0000000..02b588a
--- /dev/null
+++ b/src/test/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepositoryIntegrationTest.kt
@@ -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)
+ }
+}
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()) }
+ }
+}
diff --git a/src/test/resources/application-test.yaml b/src/test/resources/application-test.yaml
new file mode 100644
index 0000000..9aabcb5
--- /dev/null
+++ b/src/test/resources/application-test.yaml
@@ -0,0 +1,28 @@
+spring:
+ application:
+ name: MovieNight-Test
+ datasource:
+ url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ username: sa
+ password:
+ driver-class-name: org.h2.Driver
+ flyway:
+ enabled: true
+ url: jdbc:h2:mem:testdb;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
+ locations: classpath:db/migration
+ baseline-on-migrate: true
+ h2:
+ console:
+ enabled: false
+
+services:
+ user:
+ blocked-names:
+ - admin
+ - root
+ - system
+ film:
+ blocked-patterns:
+ - censored
+ - epstein
+ - python