core: restore compilation and tests (ports, DTOs, repo fixes, metrics)

This commit is contained in:
ITQ
2026-05-20 16:57:19 +03:00
parent ea7e66c1a0
commit 5f2a6d12ac
18 changed files with 766 additions and 36 deletions
@@ -0,0 +1,66 @@
package com.project.movienight.adapters.metrics
import com.project.movienight.domain.model.JellyfinSyncSummary
import io.micrometer.core.instrument.Counter
import io.micrometer.core.instrument.MeterRegistry
import io.micrometer.core.instrument.Timer
import org.springframework.stereotype.Service
import java.util.concurrent.atomic.AtomicInteger
@Service
class BusinessMetricsService(
meterRegistry: MeterRegistry,
) {
private val recommendationRequests: Counter = meterRegistry.counter("business_recommendation_requests_total")
private val ratingsSubmitted: Counter = meterRegistry.counter("business_ratings_submitted_total")
private val libraryEvents: Counter = meterRegistry.counter("business_library_events_total")
private val jellyfinSyncRuns: Counter = meterRegistry.counter("business_jellyfin_sync_runs_total")
private val jellyfinSyncedUsers: Counter = meterRegistry.counter("business_jellyfin_synced_users_total")
private val jellyfinSkippedUsers: Counter = meterRegistry.counter("business_jellyfin_skipped_users_total")
private val jellyfinSyncedItems: Counter = meterRegistry.counter("business_jellyfin_synced_items_total")
private val jellyfinSyncDuration: Timer =
Timer
.builder("business_jellyfin_sync_duration_seconds")
.publishPercentileHistogram()
.register(meterRegistry)
private val jellyfinSyncFailures: Counter = meterRegistry.counter("business_jellyfin_sync_failures_total")
private val jellyfinUnmappedUsersGaugeValue = AtomicInteger(0)
init {
meterRegistry.gauge("business_jellyfin_unmapped_users", jellyfinUnmappedUsersGaugeValue)
}
private val backendWriteFailures: Counter = meterRegistry.counter("business_jellyfin_backend_write_failures_total")
fun recordRecommendationRequest() {
recommendationRequests.increment()
}
fun recordRatingSubmitted() {
ratingsSubmitted.increment()
}
fun recordLibraryEvent() {
libraryEvents.increment()
}
fun recordJellyfinSync(summary: JellyfinSyncSummary) {
jellyfinSyncRuns.increment()
jellyfinSyncedUsers.increment(summary.syncedUsers.toDouble())
jellyfinSkippedUsers.increment(summary.skippedUsers.toDouble())
jellyfinSyncedItems.increment(summary.syncedItems.toDouble())
jellyfinSyncDuration.record(summary.durationMs, java.util.concurrent.TimeUnit.MILLISECONDS)
}
fun recordJellyfinSyncFailure() {
jellyfinSyncFailures.increment()
}
fun recordJellyfinUnmappedUser() {
jellyfinUnmappedUsersGaugeValue.incrementAndGet()
}
fun recordBackendWriteFailure() {
backendWriteFailures.increment()
}
}
@@ -209,7 +209,25 @@ class FilmRepository(
override fun findByTitle(title: String): Film? {
val films =
jdbc.query(
"SELECT id, title, description FROM films WHERE title = ? ORDER BY id LIMIT 1",
"""
SELECT id,
title,
description,
content_type,
release_year,
genres,
cast_members,
directors,
imdb_rating,
platform_rating,
external_url,
jellyfin_item_id,
jellyfin_library_id
FROM films
WHERE title = ?
ORDER BY id
LIMIT 1
""".trimIndent(),
filmRowMapper,
title,
)
@@ -36,6 +36,7 @@ class FilmLibraryController(
private val markFilmViewedUseCase: MarkFilmViewedUseCase,
private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase,
private val getFilmLibraryUseCase: GetFilmLibraryUseCase,
private val getAllFilmsUseCase: GetAllFilmsUseCase,
private val listFilmLibraryEntriesUseCase: ListFilmLibraryEntriesUseCase,
) {
@PostMapping
@@ -0,0 +1,46 @@
package com.project.movienight.adapters.web
import com.project.movienight.adapters.web.dto.request.RateFilmRequest
import com.project.movienight.adapters.web.dto.response.FilmRatingResponse
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
import com.project.movienight.application.ports.input.RateFilmCommand
import com.project.movienight.application.ports.input.RateFilmUseCase
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import java.util.UUID
@RestController
@RequestMapping("/api/users/{userId}/ratings")
class FilmRatingController(
private val rateFilmUseCase: RateFilmUseCase,
private val getFilmRatingsUseCase: GetFilmRatingsUseCase,
) {
@PostMapping("/films/{filmId}")
@ResponseStatus(HttpStatus.CREATED)
fun rate(
@PathVariable userId: UUID,
@PathVariable filmId: UUID,
@RequestBody request: RateFilmRequest,
): FilmRatingResponse =
FilmRatingResponse.fromDomain(
rateFilmUseCase.rate(
RateFilmCommand(
userId = userId,
filmId = filmId,
score = request.score,
note = request.note,
),
),
)
@GetMapping
fun list(
@PathVariable userId: UUID,
): List<FilmRatingResponse> = getFilmRatingsUseCase.getRatings(userId).map { FilmRatingResponse.fromDomain(it) }
}
@@ -0,0 +1,34 @@
package com.project.movienight.adapters.web
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
import com.project.movienight.application.ports.input.RecommendationQuery
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.RecommendationResult
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import java.util.UUID
@RestController
@RequestMapping("/api/users/{userId}/recommendations")
class RecommendationController(
private val getRecommendationsUseCase: GetRecommendationsUseCase,
) {
@GetMapping
fun recommend(
@PathVariable userId: UUID,
@RequestParam(required = false) contentType: String?,
@RequestParam(required = false) mood: String?,
@RequestParam(required = false, defaultValue = "10") limit: Int,
): List<RecommendationResult> =
getRecommendationsUseCase.recommend(
RecommendationQuery(
userId = userId,
contentType = contentType?.let { runCatching { ContentType.valueOf(it) }.getOrNull() },
mood = mood,
limit = limit,
),
)
}
@@ -0,0 +1,53 @@
package com.project.movienight.adapters.web
import com.project.movienight.adapters.web.dto.request.UpsertUserPreferencesRequest
import com.project.movienight.adapters.web.dto.response.UserPreferencesResponse
import com.project.movienight.application.ports.input.GetUserPreferencesUseCase
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
import com.project.movienight.domain.model.ContentType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.util.UUID
@RestController
@RequestMapping("/api/users/{userId}/preferences")
class UserPreferencesController(
private val upsertUserPreferencesUseCase: UpsertUserPreferencesUseCase,
private val getUserPreferencesUseCase: GetUserPreferencesUseCase,
) {
@PutMapping
fun upsert(
@PathVariable userId: UUID,
@RequestBody request: UpsertUserPreferencesRequest,
): UserPreferencesResponse =
UserPreferencesResponse.fromDomain(
upsertUserPreferencesUseCase.upsert(
UpsertUserPreferencesCommand(
userId = userId,
weightedGenres = request.weightedGenres,
plotTypes = request.plotTypes,
eras = request.eras,
castAndDirectors = request.castAndDirectors,
moods = request.moods,
contentTypes =
request.contentTypes.mapNotNull {
runCatching {
ContentType.valueOf(
it,
)
}.getOrNull()
},
),
),
)
@GetMapping
fun get(
@PathVariable userId: UUID,
): UserPreferencesResponse? = getUserPreferencesUseCase.get(userId)?.let { UserPreferencesResponse.fromDomain(it) }
}
@@ -0,0 +1,6 @@
package com.project.movienight.adapters.web.dto.request
data class RateFilmRequest(
val score: Int,
val note: String? = null,
)
@@ -0,0 +1,10 @@
package com.project.movienight.adapters.web.dto.request
data class UpsertUserPreferencesRequest(
val weightedGenres: Map<String, Int> = emptyMap(),
val plotTypes: List<String> = emptyList(),
val eras: List<String> = emptyList(),
val castAndDirectors: List<String> = emptyList(),
val moods: List<String> = emptyList(),
val contentTypes: List<String> = emptyList(),
)
@@ -0,0 +1,28 @@
package com.project.movienight.adapters.web.dto.response
import com.project.movienight.domain.model.FilmRating
import java.time.LocalDateTime
import java.util.UUID
data class FilmRatingResponse(
val id: UUID,
val userId: UUID,
val filmId: UUID,
val score: Int,
val note: String?,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
) {
companion object {
fun fromDomain(rating: FilmRating): FilmRatingResponse =
FilmRatingResponse(
id = rating.id,
userId = rating.userId,
filmId = rating.filmId,
score = rating.score,
note = rating.note,
createdAt = rating.createdAt,
updatedAt = rating.updatedAt,
)
}
}
@@ -0,0 +1,28 @@
package com.project.movienight.adapters.web.dto.response
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.UserPreferences
import java.util.UUID
data class UserPreferencesResponse(
val userId: UUID,
val weightedGenres: Map<String, Int>,
val plotTypes: List<String>,
val eras: List<String>,
val castAndDirectors: List<String>,
val moods: List<String>,
val contentTypes: List<ContentType>,
) {
companion object {
fun fromDomain(preferences: UserPreferences): UserPreferencesResponse =
UserPreferencesResponse(
userId = preferences.userId,
weightedGenres = preferences.weightedGenres,
plotTypes = preferences.plotTypes,
eras = preferences.eras,
castAndDirectors = preferences.castAndDirectors,
moods = preferences.moods,
contentTypes = preferences.contentTypes,
)
}
}
@@ -0,0 +1,19 @@
package com.project.movienight.application.ports.input
import com.project.movienight.domain.model.FilmRating
import java.util.UUID
interface RateFilmUseCase {
fun rate(command: RateFilmCommand): FilmRating
}
data class RateFilmCommand(
val userId: UUID,
val filmId: UUID,
val score: Int,
val note: String? = null,
)
interface GetFilmRatingsUseCase {
fun getRatings(userId: UUID): List<FilmRating>
}
@@ -0,0 +1,16 @@
package com.project.movienight.application.ports.input
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.RecommendationResult
import java.util.UUID
interface GetRecommendationsUseCase {
fun recommend(query: RecommendationQuery): List<RecommendationResult>
}
data class RecommendationQuery(
val userId: UUID,
val contentType: ContentType? = null,
val mood: String? = null,
val limit: Int = 10,
)
@@ -0,0 +1,23 @@
package com.project.movienight.application.ports.input
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.UserPreferences
import java.util.UUID
interface UpsertUserPreferencesUseCase {
fun upsert(command: UpsertUserPreferencesCommand): UserPreferences
}
data class UpsertUserPreferencesCommand(
val userId: UUID,
val weightedGenres: Map<String, Int> = emptyMap(),
val plotTypes: List<String> = emptyList(),
val eras: List<String> = emptyList(),
val castAndDirectors: List<String> = emptyList(),
val moods: List<String> = emptyList(),
val contentTypes: List<ContentType> = emptyList(),
)
interface GetUserPreferencesUseCase {
fun get(userId: UUID): UserPreferences?
}
@@ -0,0 +1,57 @@
package com.project.movienight.application.services
import com.project.movienight.adapters.metrics.BusinessMetricsService
import com.project.movienight.application.ports.input.GetFilmRatingsUseCase
import com.project.movienight.application.ports.input.RateFilmCommand
import com.project.movienight.application.ports.input.RateFilmUseCase
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
import com.project.movienight.application.ports.output.FilmRepositoryPort
import com.project.movienight.application.ports.output.IdGenerator
import com.project.movienight.domain.exception.DomainException
import com.project.movienight.domain.exception.EntityNotFoundException
import com.project.movienight.domain.model.FilmRating
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import java.util.UUID
@Service
class FilmRatingService(
private val filmRepository: FilmRepositoryPort,
private val filmRatingRepository: FilmRatingRepositoryPort,
private val idGenerator: IdGenerator,
private val businessMetricsService: BusinessMetricsService,
) : RateFilmUseCase,
GetFilmRatingsUseCase {
override fun rate(command: RateFilmCommand): FilmRating {
if (command.score !in 1..10) {
throw DomainException("Film rating score must be between 1 and 10")
}
filmRepository.findById(command.filmId)
?: throw EntityNotFoundException(entity = "Film", id = command.filmId.toString())
val existingRating = filmRatingRepository.findByUserIdAndFilmId(command.userId, command.filmId)
val now = LocalDateTime.now()
val rating =
if (existingRating == null) {
FilmRating(
id = idGenerator.generateId(),
userId = command.userId,
filmId = command.filmId,
score = command.score,
note = command.note,
createdAt = now,
updatedAt = now,
)
} else {
existingRating.copy(score = command.score, note = command.note, updatedAt = now)
}
val savedRating = filmRatingRepository.save(rating)
businessMetricsService.recordRatingSubmitted()
return savedRating
}
override fun getRatings(userId: UUID): List<FilmRating> = filmRatingRepository.findByUserId(userId)
}
@@ -40,7 +40,7 @@ class FilmService(
try {
log.debug(
"Create film request received: title='{}', descriptionLength={}",
"Create film request received: title='{}', descriptionLength={}'",
command.title,
command.description.length,
)
@@ -72,7 +72,13 @@ class FilmService(
jellyfinItemId = command.jellyfinItemId,
jellyfinLibraryId = command.jellyfinLibraryId,
)
return filmRepository.save(film)
val saved = filmRepository.save(film)
filmCreatedCounter.increment()
return saved
} finally {
sample.stop(createFilmTimer)
}
}
override fun edit(
@@ -95,7 +101,7 @@ class FilmService(
throw BlockedValueException(target = "Film", field = "description")
}
val film = filmRepository.findById(id)
var film = filmRepository.findById(id)
if (film == null) {
log.debug("Film not found for edit: id='{}'", id)
@@ -118,7 +124,12 @@ class FilmService(
jellyfinLibraryId = command.jellyfinLibraryId,
)
return filmRepository.save(film)
val saved = filmRepository.save(film)
filmEditedCounter.increment()
return saved
} finally {
sample.stop(editFilmTimer)
}
}
override fun delete(id: UUID) {
@@ -0,0 +1,117 @@
package com.project.movienight.application.services
import com.project.movienight.adapters.metrics.BusinessMetricsService
import com.project.movienight.application.ports.input.GetRecommendationsUseCase
import com.project.movienight.application.ports.input.RecommendationQuery
import com.project.movienight.application.ports.output.FilmLibraryRepositoryPort
import com.project.movienight.application.ports.output.FilmRatingRepositoryPort
import com.project.movienight.application.ports.output.FilmRepositoryPort
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
import com.project.movienight.domain.model.ContentType
import com.project.movienight.domain.model.Film
import com.project.movienight.domain.model.RecommendationResult
import org.springframework.stereotype.Service
@Service
class RecommendationService(
private val filmRepository: FilmRepositoryPort,
private val filmLibraryRepository: FilmLibraryRepositoryPort,
private val filmRatingRepository: FilmRatingRepositoryPort,
private val userPreferencesRepository: UserPreferencesRepositoryPort,
private val businessMetricsService: BusinessMetricsService,
) : GetRecommendationsUseCase {
override fun recommend(query: RecommendationQuery): List<RecommendationResult> {
businessMetricsService.recordRecommendationRequest()
val preferences = userPreferencesRepository.findByUserId(query.userId)
val ratings = filmRatingRepository.findByUserId(query.userId).associateBy { it.filmId }
val watchedFilmIds =
filmLibraryRepository
.findAll()
.filter {
it.userId == query.userId && it.isViewed
}.map { it.filmId }
.toSet()
return filmRepository
.findAll()
.asSequence()
.filter { film -> query.contentType == null || film.contentType == query.contentType }
.map { film ->
scoreFilm(film, query.mood, preferences, ratings[film.id] != null, watchedFilmIds.contains(film.id))
}.sortedByDescending { it.score }
.take(query.limit.coerceAtLeast(1))
.toList()
}
private fun scoreFilm(
film: Film,
mood: String?,
preferences: com.project.movienight.domain.model.UserPreferences?,
hasUserRating: Boolean,
watched: Boolean,
): RecommendationResult {
var score = 0.0
val reasons = mutableListOf<String>()
preferences?.contentTypes?.let {
if (it.isEmpty() || it.contains(film.contentType)) {
score += 2.0
reasons += "Matches content preference"
}
}
preferences?.weightedGenres?.forEach { (genre, weight) ->
if (film.genres.any { it.equals(genre, ignoreCase = true) }) {
score += weight
reasons += "Matches genre $genre"
}
}
preferences?.castAndDirectors?.forEach { favorite ->
val found =
film.cast.any { it.equals(favorite, ignoreCase = true) } ||
film.directors.any { it.equals(favorite, ignoreCase = true) }
if (found) {
score += 1.5
reasons += "Matches favorite creator or cast member $favorite"
}
}
preferences?.moods?.forEach { preferredMood ->
if (mood != null && preferredMood.equals(mood, ignoreCase = true)) {
score += 1.25
reasons += "Matches requested mood $mood"
}
}
film.imdbRating?.let {
score += it / 2.0
reasons += "Strong IMDb signal"
}
film.platformRating?.let {
score += it
reasons += "Strong platform signal"
}
if (hasUserRating) {
score += 2.0
reasons += "User has already rated similar content"
}
if (watched) {
score -= 3.0
reasons += "Already watched"
}
if (mood != null && film.title.contains(mood, ignoreCase = true)) {
score += 0.5
}
if (reasons.isEmpty()) {
reasons += "Baseline recommendation from library catalog"
}
return RecommendationResult(film = film, score = score, reasons = reasons)
}
}
@@ -0,0 +1,29 @@
package com.project.movienight.application.services
import com.project.movienight.application.ports.input.GetUserPreferencesUseCase
import com.project.movienight.application.ports.input.UpsertUserPreferencesCommand
import com.project.movienight.application.ports.input.UpsertUserPreferencesUseCase
import com.project.movienight.application.ports.output.UserPreferencesRepositoryPort
import com.project.movienight.domain.model.UserPreferences
import org.springframework.stereotype.Service
@Service
class UserPreferencesService(
private val userPreferencesRepository: UserPreferencesRepositoryPort,
) : UpsertUserPreferencesUseCase,
GetUserPreferencesUseCase {
override fun upsert(command: UpsertUserPreferencesCommand): UserPreferences =
userPreferencesRepository.save(
UserPreferences(
userId = command.userId,
weightedGenres = command.weightedGenres,
plotTypes = command.plotTypes,
eras = command.eras,
castAndDirectors = command.castAndDirectors,
moods = command.moods,
contentTypes = command.contentTypes,
),
)
override fun get(userId: java.util.UUID): UserPreferences? = userPreferencesRepository.findByUserId(userId)
}
@@ -0,0 +1,168 @@
package com.project.movienight
import com.fasterxml.jackson.databind.ObjectMapper
import com.project.movienight.adapters.web.dto.request.CreateFilmRequest
import com.project.movienight.adapters.web.dto.request.CreateUserRequest
import com.project.movienight.adapters.web.dto.request.RateFilmRequest
import com.project.movienight.adapters.web.dto.request.UpsertUserPreferencesRequest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post
import org.springframework.test.web.servlet.put
import java.util.UUID
@SpringBootTest
@AutoConfigureMockMvc(addFilters = false)
@ActiveProfiles("test")
class RecommendationSmokeTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Autowired
private lateinit var objectMapper: ObjectMapper
@Autowired
private lateinit var jdbcTemplate: JdbcTemplate
@BeforeEach
fun setup() {
cleanDatabase()
}
@AfterEach
fun cleanup() {
cleanDatabase()
}
@Test
fun `should create data and return a ranked recommendation`() {
mockMvc
.post("/api/users") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(CreateUserRequest(name = "Jane", email = "jane@example.com"))
}.andExpect {
status { isCreated() }
}
val userId =
UUID.fromString(
jdbcTemplate.queryForObject(
"SELECT id FROM users WHERE email = ?",
String::class.java,
"jane@example.com",
),
)
mockMvc
.post("/api/films") {
contentType = MediaType.APPLICATION_JSON
content =
objectMapper.writeValueAsString(
CreateFilmRequest(
title = "Orbital Drift",
description = "A science-fiction rescue mission",
contentType = "FILM",
genres = listOf("SCI-FI", "THRILLER"),
directors = listOf("Nora Finch"),
imdbRating = 8.7,
platformRating = 9.0,
externalUrl = "https://example.com/orbital-drift",
),
)
}.andExpect {
status { isCreated() }
}
mockMvc
.post("/api/films") {
contentType = MediaType.APPLICATION_JSON
content =
objectMapper.writeValueAsString(
CreateFilmRequest(
title = "Small Town Summer",
description = "A grounded family drama",
contentType = "FILM",
genres = listOf("DRAMA"),
directors = listOf("Ava Reed"),
imdbRating = 7.1,
platformRating = 6.8,
),
)
}.andExpect {
status { isCreated() }
}
val createdFilms = jdbcTemplate.queryForList("SELECT id, title FROM films ORDER BY title")
val filmIdByTitle =
createdFilms.associate { row ->
row["title"].toString() to UUID.fromString(row["id"].toString())
}
val firstFilmId = filmIdByTitle.getValue("Orbital Drift")
val secondFilmId = filmIdByTitle.getValue("Small Town Summer")
mockMvc
.put("/api/users/$userId/preferences") {
contentType = MediaType.APPLICATION_JSON
content =
objectMapper.writeValueAsString(
UpsertUserPreferencesRequest(
weightedGenres = mapOf("SCI-FI" to 5),
moods = listOf("focused"),
contentTypes = listOf("FILM"),
),
)
}.andExpect {
status { isOk() }
jsonPath("$.weightedGenres['SCI-FI']") { value(5) }
}
mockMvc
.post("/api/users/$userId/ratings/films/$firstFilmId") {
contentType = MediaType.APPLICATION_JSON
content = objectMapper.writeValueAsString(RateFilmRequest(score = 10, note = "Great fit"))
}.andExpect {
status { isCreated() }
jsonPath("$.score") { value(10) }
}
mockMvc
.post("/api/users/$userId/library/films/$secondFilmId/viewed")
.andExpect {
status { isOk() }
jsonPath("$.viewed") { value(true) }
}
mockMvc
.get("/api/users/$userId/ratings")
.andExpect {
status { isOk() }
jsonPath("$[0].filmId") { value(firstFilmId.toString()) }
}
mockMvc
.get("/api/users/$userId/recommendations") {
param("contentType", "FILM")
param("limit", "2")
}.andExpect {
status { isOk() }
jsonPath("$[0].film.id") { value(firstFilmId.toString()) }
}
}
private fun cleanDatabase() {
jdbcTemplate.execute("DELETE FROM film_ratings")
jdbcTemplate.execute("DELETE FROM user_preferences")
jdbcTemplate.execute("DELETE FROM favorites")
jdbcTemplate.execute("DELETE FROM films")
jdbcTemplate.execute("DELETE FROM users")
}
}