From c8d7813deb4794daba394a93f9d839eb9fef33e3 Mon Sep 17 00:00:00 2001 From: andrsam Date: Sun, 30 Mar 2025 10:07:03 +0300 Subject: [PATCH 1/3] 2609 [BE] new user analytics functionality using database table --- README.md | 3 +- .../brn/config/UserDetailControllerConfig.kt | 10 +++ .../brn/controller/UserDetailController.kt | 8 +- .../dto/response/UserWithAnalyticsResponse.kt | 2 +- .../com/epam/brn/job/UserAnalyticsJob.kt | 50 +++++++++++ .../com/epam/brn/model/UserAnalytics.kt | 37 ++++++++ .../projection/UsersWithAnalyticsView.kt | 25 ++++++ .../epam/brn/repo/UserAnalyticsRepository.kt | 31 +++++++ .../brn/service/UserAnalyticsServiceV1.kt | 8 ++ .../service/impl/UserAnalyticsServiceImpl.kt | 12 +-- .../impl/UserAnalyticsServiceV1Impl.kt | 33 +++++++ src/main/resources/application-dev.properties | 5 +- src/main/resources/application.properties | 4 + .../db/migration/V220241201_2609.sql | 17 ++++ .../controller/UserDetailControllerTest.kt | 32 ++++++- .../com/epam/brn/job/UserAnalyticsJobTest.kt | 31 +++++++ .../impl/UserAnalyticsServiceV1ImplTest.kt | 87 +++++++++++++++++++ src/test/resources/application.properties | 5 ++ 18 files changed, 384 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt create mode 100644 src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt create mode 100644 src/main/kotlin/com/epam/brn/model/UserAnalytics.kt create mode 100644 src/main/kotlin/com/epam/brn/model/projection/UsersWithAnalyticsView.kt create mode 100644 src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt create mode 100644 src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt create mode 100644 src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt create mode 100644 src/main/resources/db/migration/V220241201_2609.sql create mode 100644 src/test/kotlin/com/epam/brn/job/UserAnalyticsJobTest.kt create mode 100644 src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt diff --git a/README.md b/README.md index 878ee474f..f36e569b5 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ docker run --name postgres_dev -d -p 5432:5432 -e POSTGRES_DB=brn -e POSTGRES_PA ```bash docker run --name postgres_dev -d -p 5432:5432 -e POSTGRES_DB=brn -e POSTGRES_PASSWORD=admin -e POSTGRES_USER=admin postgres:13 ``` +if you want container start automatically on system boot you must use --restart=always option ### Back end Kotlin Part: 1. Run command 'gradle build' from main project folder to build project with tests. @@ -172,7 +173,7 @@ docker rm $(docker ps -a -q) # Remove all stopped containers https://github.com/Brain-up/brn/wiki/Kotlin-request-dto-validation-with-annotations ### Flyway scripts naming -use `V2yearmonthday_taskNumber` +use `V2yyyymmdd_taskNumber` for example `V220210804_899`. ### Branches: diff --git a/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt b/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt new file mode 100644 index 000000000..c4246060a --- /dev/null +++ b/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt @@ -0,0 +1,10 @@ +package com.epam.brn.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration + +@Configuration +class UserDetailControllerConfig( + @Value("\${brn.user.analytics.use.new.version}") + val isUseNewAnalyticsService: Boolean +) diff --git a/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt b/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt index bee36703a..caf611bd0 100644 --- a/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt +++ b/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt @@ -1,5 +1,6 @@ package com.epam.brn.controller +import com.epam.brn.config.UserDetailControllerConfig import com.epam.brn.dto.HeadphonesDto import com.epam.brn.dto.UserAccountDto import com.epam.brn.dto.request.UserAccountChangeRequest @@ -8,6 +9,7 @@ import com.epam.brn.enums.BrnRole import com.epam.brn.service.DoctorService import com.epam.brn.service.UserAccountService import com.epam.brn.service.UserAnalyticsService +import com.epam.brn.service.UserAnalyticsServiceV1 import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import org.springframework.data.domain.Pageable @@ -35,7 +37,9 @@ import javax.annotation.security.RolesAllowed class UserDetailController( private val userAccountService: UserAccountService, private val doctorService: DoctorService, - private val userAnalyticsService: UserAnalyticsService + private val config: UserDetailControllerConfig, + private val userAnalyticsService: UserAnalyticsService, + private val userAnalyticsServiceV1: UserAnalyticsServiceV1 ) { @GetMapping @Operation(summary = "Get all users with/without analytic data") @@ -45,7 +49,7 @@ class UserDetailController( @RequestParam("role", defaultValue = "USER") role: String, @PageableDefault pageable: Pageable, ): ResponseEntity { - val users = if (withAnalytics) userAnalyticsService.getUsersWithAnalytics(pageable, role) + val users = if (withAnalytics) if (config.isUseNewAnalyticsService) userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role) else userAnalyticsService.getUsersWithAnalytics(pageable, role) else userAccountService.getUsers(pageable, role) return ResponseEntity.ok().body(BrnResponse(data = users)) } diff --git a/src/main/kotlin/com/epam/brn/dto/response/UserWithAnalyticsResponse.kt b/src/main/kotlin/com/epam/brn/dto/response/UserWithAnalyticsResponse.kt index 6a0824bd0..f05a3c174 100644 --- a/src/main/kotlin/com/epam/brn/dto/response/UserWithAnalyticsResponse.kt +++ b/src/main/kotlin/com/epam/brn/dto/response/UserWithAnalyticsResponse.kt @@ -18,7 +18,7 @@ data class UserWithAnalyticsResponse( var active: Boolean = true, var firstDone: LocalDateTime? = null, // generally first done exercise var lastDone: LocalDateTime? = null, // generally last done exercise - var lastWeek: List = emptyList(), + var lastWeek: MutableList = arrayListOf(), var studyDaysInCurrentMonth: Int = 0, // amount of days in current month when user made any exercises var diagnosticProgress: Map = mapOf(AudiometryType.SIGNALS to true), // todo fill by user var doneExercises: Int = 0, // for all time diff --git a/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt b/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt new file mode 100644 index 000000000..44c522496 --- /dev/null +++ b/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt @@ -0,0 +1,50 @@ +package com.epam.brn.job + +import org.apache.logging.log4j.kotlin.logger +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional + +@Component +@ConditionalOnProperty(name = ["brn.user.analytics.job.enabled"], havingValue = "true") +class UserAnalyticsJob( + private val jdbcTemplate: JdbcTemplate +) { + private val log = logger() + @Scheduled(cron = "@midnight") + @Transactional + fun fillUserAnalytics() { + try { + log.info("start filling study analytics table...") + val rowsCount = jdbcTemplate.update(FILL_USER_ANALYTICS_SQL) + log.info("filling study analytics table was finished successfully. total $rowsCount rows inserted") + } catch (e: Exception) { + log.error("Some error occurred on fill statistics tables: ${e.message}", e) + } + } +} + +private const val FILL_USER_ANALYTICS_SQL: String = """ + TRUNCATE TABLE user_analytics CASCADE; + INSERT INTO user_analytics (user_id, first_done, last_done, spent_time, + done_exercises, study_days, role_name) + SELECT s.user_id, + min(start_time), + max(start_time), + coalesce(sum(spent_time_in_seconds), 0), + count(distinct exercise_id), + (SELECT distinct count(distinct date_trunc('day', s1.start_time)) + FROM study_history s1 + WHERE s1.user_id = s.user_id + AND s1.start_time between date_trunc('month', current_date) + AND date_trunc('month', current_date) + interval '1 month - 1 microsecond'), + r.name + FROM study_history s, + user_roles ur, + role r + WHERE s.user_id = ur.user_id + AND ur.role_id = r.id + GROUP BY s.user_id, r.name; + """ diff --git a/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt b/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt new file mode 100644 index 000000000..d34d92f2a --- /dev/null +++ b/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt @@ -0,0 +1,37 @@ +package com.epam.brn.model + +import java.time.LocalDateTime +import javax.persistence.Column +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Index +import javax.persistence.Entity +import javax.persistence.Table +import javax.persistence.Id + +@Entity +@Table(indexes = [Index(name = "user_analytics_ix_role_name", columnList = "role_name")]) +class UserAnalytics( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val id: Long? = null, + + val userId: Long, + + val firstDone: LocalDateTime?, + + val lastDone: LocalDateTime?, + + val spentTime: Long?, + + val doneExercises: Int?, + + val studyDays: Int?, + + @Column(name = "role_name") + val roleName: String +) { + override fun toString(): String { + return "UserAnalytics(id=$id, userId=$userId, firstDone=$firstDone, lastDone=$lastDone, spentTime=$spentTime, doneExercises=$doneExercises, studyDays=$studyDays, roleName='$roleName')" + } +} diff --git a/src/main/kotlin/com/epam/brn/model/projection/UsersWithAnalyticsView.kt b/src/main/kotlin/com/epam/brn/model/projection/UsersWithAnalyticsView.kt new file mode 100644 index 000000000..61645c92f --- /dev/null +++ b/src/main/kotlin/com/epam/brn/model/projection/UsersWithAnalyticsView.kt @@ -0,0 +1,25 @@ +package com.epam.brn.model.projection + +import com.epam.brn.enums.BrnGender +import java.time.LocalDateTime + +/** + * StudyAnalyticsView. + * + * @author Andrey Samoylov + */ +interface UsersWithAnalyticsView { + val id: Long + val userId: String + val fullName: String + val email: String + val bornYear: Int? + val gender: BrnGender + var active: Boolean + val firstDone: LocalDateTime + val lastDone: LocalDateTime + var lastVisit: LocalDateTime + val doneExercises: Int + val spentTime: Long + val studyDays: Int +} diff --git a/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt b/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt new file mode 100644 index 000000000..57d82ec25 --- /dev/null +++ b/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt @@ -0,0 +1,31 @@ +package com.epam.brn.repo + +import com.epam.brn.model.UserAnalytics +import com.epam.brn.model.projection.UsersWithAnalyticsView +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query + +interface UserAnalyticsRepository : JpaRepository { + @Query( + """ + select u.id as id, + u.userId as userId, + u.fullName as fullName, + u.email as email, + u.bornYear as bornYear, + u.gender as gender, + u.active as active, + u.lastVisit as lastVisit, + a.firstDone as firstDone, + a.lastDone as lastDone, + a.doneExercises as doneExercises, + a.spentTime as spentTime, + a.studyDays as studyDays + from UserAnalytics a + join UserAccount u on a.userId = u.id + where a.roleName=:roleName + """ + ) + fun getUserAnalytics(pageable: Pageable, roleName: String): List +} diff --git a/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt b/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt new file mode 100644 index 000000000..8a5138d7c --- /dev/null +++ b/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt @@ -0,0 +1,8 @@ +package com.epam.brn.service + +import com.epam.brn.dto.response.UserWithAnalyticsResponse +import org.springframework.data.domain.Pageable + +interface UserAnalyticsServiceV1 { + fun getUsersWithAnalytics(pageable: Pageable, role: String): List +} diff --git a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt index aa5f570e0..113d91b03 100644 --- a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt +++ b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceImpl.kt @@ -53,17 +53,17 @@ class UserAnalyticsServiceImpl( val startOfCurrentMonth = now.withDayOfMonth(1).with(LocalTime.MIN) users.onEach { user -> - user.lastWeek = userDayStatisticsService.getStatisticsForPeriod(from, to, user.id) + user.lastWeek = userDayStatisticsService.getStatisticsForPeriod(from, to, user.id).toMutableList() user.studyDaysInCurrentMonth = countWorkDaysForMonth( userDayStatisticsService.getStatisticsForPeriod(startOfCurrentMonth, now, user.id) ) - val userStatistic = studyHistoryRepository.getStatisticsByUserAccountId(user.id) + val userStatistics = studyHistoryRepository.getStatisticsByUserAccountId(user.id) user.apply { - this.firstDone = userStatistic.firstStudy - this.lastDone = userStatistic.lastStudy - this.spentTime = userStatistic.spentTime.toDuration(DurationUnit.SECONDS) - this.doneExercises = userStatistic.doneExercises + this.firstDone = userStatistics.firstStudy + this.lastDone = userStatistics.lastStudy + this.spentTime = userStatistics.spentTime.toDuration(DurationUnit.SECONDS) + this.doneExercises = userStatistics.doneExercises } } return users diff --git a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt new file mode 100644 index 000000000..40f30a08d --- /dev/null +++ b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt @@ -0,0 +1,33 @@ +package com.epam.brn.service.impl + +import com.epam.brn.dto.response.UserWithAnalyticsResponse +import com.epam.brn.repo.UserAnalyticsRepository +import com.epam.brn.service.UserAnalyticsServiceV1 +import org.springframework.data.domain.Pageable +import org.springframework.stereotype.Service +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@Service +class UserAnalyticsServiceV1Impl(private val userAnalyticsRepository: UserAnalyticsRepository) : + UserAnalyticsServiceV1 { + + override fun getUsersWithAnalytics(pageable: Pageable, role: String): List = + userAnalyticsRepository.getUserAnalytics(pageable, role).map { + UserWithAnalyticsResponse( + id = it.id, + userId = it.userId, + name = it.fullName, + active = it.active, + email = it.email, + bornYear = it.bornYear, + gender = it.gender, + firstDone = it.firstDone, + lastDone = it.lastDone, + lastVisit = it.lastVisit, + doneExercises = it.doneExercises, + spentTime = it.spentTime.toDuration(DurationUnit.SECONDS), + studyDaysInCurrentMonth = it.studyDays + ) + } +} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 042bb688a..f90108c0b 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,7 +1,8 @@ spring.datasource.username=${POSTGRES_USER:admin} spring.datasource.password=${POSTGRES_PASSWORD:admin} -spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${POSTGRES_DB:brn} - +spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5431}/${POSTGRES_DB:brn} +brn.user.analytics.use.new.version=true +brn.user.analytics.job.enabled=true #logging.level.org.springframework=debug #spring.jpa.show-sql=true diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5389f1e5f..89d3f165c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -90,6 +90,10 @@ brn.resources.pictures.update-job.cron = 0 0 0 1 * * #brn.resources.pictures.update-job.cron = 0 0/1 * * * ? brn.dataFormatNumLines=5 + +brn.user.analytics.use.new.version=${USE_NEW_USER_ANALYTICS_VERSION:true} +brn.user.analytics.job.enabled=${USER_ANALYTICS_JOB_ENABLED:true} + brn.statistics.progress.day.status.bad.minimal=0 brn.statistics.progress.day.status.bad.maximal=15 brn.statistics.progress.day.status.good.minimal=15 diff --git a/src/main/resources/db/migration/V220241201_2609.sql b/src/main/resources/db/migration/V220241201_2609.sql new file mode 100644 index 000000000..c71d65578 --- /dev/null +++ b/src/main/resources/db/migration/V220241201_2609.sql @@ -0,0 +1,17 @@ + +create table user_analytics +( + id bigint generated by default as identity + primary key, + done_exercises integer, + first_done timestamp, + last_done timestamp, + role_name varchar(255), + spent_time bigint, + study_days integer, + user_id bigint +); + +create index user_analytics_ix_role_name + on user_analytics (role_name); + diff --git a/src/test/kotlin/com/epam/brn/controller/UserDetailControllerTest.kt b/src/test/kotlin/com/epam/brn/controller/UserDetailControllerTest.kt index 2e5087d15..8dfd72246 100644 --- a/src/test/kotlin/com/epam/brn/controller/UserDetailControllerTest.kt +++ b/src/test/kotlin/com/epam/brn/controller/UserDetailControllerTest.kt @@ -1,5 +1,6 @@ package com.epam.brn.controller +import com.epam.brn.config.UserDetailControllerConfig import com.epam.brn.dto.HeadphonesDto import com.epam.brn.dto.UserAccountDto import com.epam.brn.dto.request.UserAccountChangeRequest @@ -12,11 +13,12 @@ import com.epam.brn.enums.HeadphonesType import com.epam.brn.service.DoctorService import com.epam.brn.service.UserAccountService import com.epam.brn.service.UserAnalyticsService -import com.google.firebase.auth.FirebaseAuth +import com.epam.brn.service.UserAnalyticsServiceV1 import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.impl.annotations.InjectMockKs import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK import io.mockk.junit5.MockKExtension import io.mockk.justRun import io.mockk.mockk @@ -39,11 +41,11 @@ internal class UserDetailControllerTest { @InjectMockKs lateinit var userDetailController: UserDetailController - @MockK - lateinit var userAccountService: UserAccountService + @RelaxedMockK + lateinit var config: UserDetailControllerConfig @MockK - lateinit var firebaseAuth: FirebaseAuth + lateinit var userAccountService: UserAccountService @MockK private lateinit var doctorService: DoctorService @@ -51,6 +53,9 @@ internal class UserDetailControllerTest { @MockK private lateinit var userAnalyticsService: UserAnalyticsService + @MockK + private lateinit var userAnalyticsServiceV1: UserAnalyticsServiceV1 + lateinit var userAccountDto: UserAccountDto val userId: Long = NumberUtils.LONG_ONE @@ -298,6 +303,25 @@ internal class UserDetailControllerTest { (users.body as BrnResponse<*>).data shouldBe listOf(userWithAnalyticsResponse) } + @Test + fun `getUsers should return users with statistics when withAnalytics is true for V1 analytics service version`() { + // GIVEN + val withAnalytics = true + val role = BrnRole.USER + val pageable = mockk() + val userWithAnalyticsResponse = mockk() + every { config.isUseNewAnalyticsService } returns true + every { userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role) } returns listOf(userWithAnalyticsResponse) + + // WHEN + val users = userDetailController.getUsers(withAnalytics, role, pageable) + + // THEN + verify(exactly = 1) { userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role) } + users.statusCodeValue shouldBe HttpStatus.SC_OK + (users.body as BrnResponse<*>).data shouldBe listOf(userWithAnalyticsResponse) + } + @Test fun `getUsers should return users when withAnalytics is false`() { // GIVEN diff --git a/src/test/kotlin/com/epam/brn/job/UserAnalyticsJobTest.kt b/src/test/kotlin/com/epam/brn/job/UserAnalyticsJobTest.kt new file mode 100644 index 000000000..bee4f36df --- /dev/null +++ b/src/test/kotlin/com/epam/brn/job/UserAnalyticsJobTest.kt @@ -0,0 +1,31 @@ +package com.epam.brn.job + +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.jdbc.core.JdbcTemplate + +@ExtendWith(MockKExtension::class) +class UserAnalyticsJobTest { + @InjectMockKs + lateinit var userAnalyticsJob: UserAnalyticsJob + + @MockK(relaxed = true, relaxUnitFun = true) + lateinit var jdbcTemplate: JdbcTemplate + + @Test + fun fillStudyAnalytics() { + // GIVEN + every { jdbcTemplate.update(any()) }.returns(1) + + // WHEN + userAnalyticsJob.fillUserAnalytics() + + // THEN + verify(exactly = 1) { jdbcTemplate.update(any()) } + } +} diff --git a/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt b/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt new file mode 100644 index 000000000..3466c3a39 --- /dev/null +++ b/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt @@ -0,0 +1,87 @@ +package com.epam.brn.service.impl + +import com.epam.brn.enums.BrnGender +import com.epam.brn.model.projection.UsersWithAnalyticsView +import com.epam.brn.repo.UserAnalyticsRepository +import io.kotest.inspectors.forExactly +import io.mockk.every +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.data.domain.Pageable +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +private const val ONE = 1 +private const val ONE_LONG = 1L + +@ExtendWith(MockKExtension::class) +class UserAnalyticsServiceV1ImplTest { + + @InjectMockKs + lateinit var service: UserAnalyticsServiceV1Impl + + @MockK + lateinit var userAnalyticsRepository: UserAnalyticsRepository + + @MockK + lateinit var pageable: Pageable + + @MockK + lateinit var usersWithAnalyticsView: UsersWithAnalyticsView + + @Test + fun getUsersWithAnalytics() { + // GIVEN + val role = "USER" + val now = LocalDateTime.now() + val firstDone = now.minusHours(ONE_LONG).truncatedTo(ChronoUnit.SECONDS) + val lastDone = now.plusHours(ONE_LONG).truncatedTo(ChronoUnit.SECONDS) + val bornYear = 2025 + + every { usersWithAnalyticsView.id } returns ONE_LONG + every { usersWithAnalyticsView.userId } returns "1" + every { usersWithAnalyticsView.fullName } returns "Test User" + every { usersWithAnalyticsView.email } returns "test@test.com" + every { usersWithAnalyticsView.bornYear } returns bornYear + every { usersWithAnalyticsView.gender } returns BrnGender.MALE + every { usersWithAnalyticsView.active } returns true + every { usersWithAnalyticsView.firstDone } returns firstDone + every { usersWithAnalyticsView.lastDone } returns lastDone + every { usersWithAnalyticsView.lastVisit } returns now + every { usersWithAnalyticsView.doneExercises } returns ONE + every { usersWithAnalyticsView.spentTime } returns ONE_LONG + every { usersWithAnalyticsView.studyDays } returns ONE + + every { userAnalyticsRepository.getUserAnalytics(any(), any()) } returns listOf(usersWithAnalyticsView) + + // WHEN + val usersWithAnalytics = service.getUsersWithAnalytics(pageable, role) + + // THEN + verify(exactly = ONE) { userAnalyticsRepository.getUserAnalytics(pageable, role) } + + usersWithAnalytics.forExactly(ONE) { + it.id shouldBe usersWithAnalyticsView.id + it.userId shouldBe usersWithAnalyticsView.userId + it.name shouldBe usersWithAnalyticsView.fullName + it.email shouldBe usersWithAnalyticsView.email + it.bornYear shouldBe usersWithAnalyticsView.bornYear + it.gender shouldBe usersWithAnalyticsView.gender + it.active shouldBe usersWithAnalyticsView.active + it.firstDone shouldBe usersWithAnalyticsView.firstDone + it.lastDone shouldBe usersWithAnalyticsView.lastDone + it.lastVisit shouldBe usersWithAnalyticsView.lastVisit + it.doneExercises shouldBe usersWithAnalyticsView.doneExercises + it.spentTime shouldBeEqualTo usersWithAnalyticsView.spentTime.toDuration(DurationUnit.SECONDS) + it.studyDaysInCurrentMonth shouldBe usersWithAnalyticsView.studyDays + } + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index c90744a63..8505d971e 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -8,6 +8,7 @@ spring.mvc.format.date-time=iso spring.jpa.show-sql=true #To beautify or pretty print the SQL, we can add: spring.jpa.properties.hibernate.format_sql=true +#spring.jpa.hibernate.ddl-auto=update #spring.datasource.url=jdbc:tc:postgresql:11-alpine://localhost:5432/testdb createOrUpdate=true @@ -62,6 +63,10 @@ brn.resources.pictures.ext=png brn.resources.pictures.update-job.cron = 0 0 0 1 * * brn.dataFormatNumLines=5 + +brn.user.analytics.use.new.version=true +brn.user.analytics.job.enabled=true + brn.statistics.progress.day.status.bad.minimal=0 brn.statistics.progress.day.status.bad.maximal=15 brn.statistics.progress.day.status.good.minimal=15 From 257ce5079db6b109872e2adf393d6050d4a462ba Mon Sep 17 00:00:00 2001 From: andrsam Date: Tue, 1 Apr 2025 11:39:17 +0300 Subject: [PATCH 2/3] 2609 [BE] user analytics job integration test --- .../brn/integration/UserAnalyticsJobIT.kt | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt diff --git a/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt new file mode 100644 index 000000000..9d53f274b --- /dev/null +++ b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt @@ -0,0 +1,67 @@ +package com.epam.brn.integration + +import com.epam.brn.job.UserAnalyticsJob +import com.epam.brn.repo.StudyHistoryRepository +import com.epam.brn.repo.UserAccountRepository +import com.epam.brn.repo.UserAnalyticsRepository +import io.kotest.inspectors.forExactly +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +class UserAnalyticsJobIT : BaseIT() { + @Autowired + lateinit var userAnalyticsJob: UserAnalyticsJob + + @Autowired + lateinit var userAnalyticsRepository: UserAnalyticsRepository + + @Autowired + private lateinit var userAccountRepository: UserAccountRepository + + @Autowired + lateinit var studyHistoryRepository: StudyHistoryRepository + + @AfterEach + fun deleteAfterTest() { + userAccountRepository.deleteAll() + } + + @Test + fun `test filling user analytics job`() { + // GIVEN + val roleName = "USER" + val role = createRole(roleName) + val user = insertDefaultUser() + user.roleSet.add(role) + userAccountRepository.save(user) + + val firstName = "FirstName" + val secondName = "SecondName" + val existingSeries = insertDefaultSeries() + val subGroup = insertDefaultSubGroup(existingSeries, 1) + val exerciseFirst = insertDefaultExercise(subGroup, firstName) + val exerciseSecond = insertDefaultExercise(subGroup, secondName) + val now = LocalDateTime.now() + val firstStudyHistory = insertDefaultStudyHistory(user, exerciseFirst, now.minusHours(1L).truncatedTo(ChronoUnit.SECONDS)) + val secondStudyHistory = insertDefaultStudyHistory(user, exerciseSecond, now.plusHours(1L).truncatedTo(ChronoUnit.SECONDS)) + + // WHEN + userAnalyticsJob.fillUserAnalytics() + + // THEN + val userAnalyticsList = userAnalyticsRepository.findAll() + userAnalyticsList.forExactly(1) { + it.userId shouldBe user.id + it.firstDone shouldBe firstStudyHistory.startTime + it.lastDone shouldBe secondStudyHistory.startTime + it.spentTime shouldBe (firstStudyHistory.spentTimeInSeconds ?: 0L) + (secondStudyHistory.spentTimeInSeconds ?: 0L) + it.doneExercises shouldBe 2 + it.studyDays shouldBe 1 + it.roleName shouldBe roleName + } + } +} From 922c53a0ab23387ef1d9753ff873a55d88e352d3 Mon Sep 17 00:00:00 2001 From: andrsam Date: Sat, 12 Apr 2025 11:04:19 +0300 Subject: [PATCH 3/3] 2609 [BE] user analytics job services comparison test --- src/main/kotlin/com/epam/brn/Application.kt | 1 + .../kotlin/com/epam/brn/config/AwsConfig.kt | 1 + .../com/epam/brn/config/SwaggerConfig.kt | 10 +-- .../brn/config/UserDetailControllerConfig.kt | 2 +- .../brn/controller/UserDetailController.kt | 9 +- .../com/epam/brn/job/UserAnalyticsJob.kt | 7 +- .../com/epam/brn/model/UserAnalytics.kt | 30 ++++--- .../epam/brn/repo/UserAnalyticsRepository.kt | 7 +- .../com/epam/brn/service/TaskService.kt | 3 +- .../epam/brn/service/UserAccountService.kt | 17 ++++ .../epam/brn/service/UserAnalyticsService.kt | 2 + .../brn/service/UserAnalyticsServiceV1.kt | 5 +- .../impl/UserAnalyticsServiceV1Impl.kt | 44 +++++----- .../resources/firebase-brainupspb-dev.json | 4 +- .../integration/UserAnalyticsComparisonIT.kt | 86 +++++++++++++++++++ .../brn/integration/UserAnalyticsJobIT.kt | 2 +- .../impl/UserAnalyticsServiceV1ImplTest.kt | 1 - src/test/resources/db/init-study-history.sql | 35 ++++++++ 18 files changed, 215 insertions(+), 51 deletions(-) create mode 100644 src/test/kotlin/com/epam/brn/integration/UserAnalyticsComparisonIT.kt create mode 100644 src/test/resources/db/init-study-history.sql diff --git a/src/main/kotlin/com/epam/brn/Application.kt b/src/main/kotlin/com/epam/brn/Application.kt index 1dd97b999..ce8fdc7f1 100644 --- a/src/main/kotlin/com/epam/brn/Application.kt +++ b/src/main/kotlin/com/epam/brn/Application.kt @@ -7,6 +7,7 @@ import org.springframework.scheduling.annotation.EnableScheduling @SpringBootApplication @EnableScheduling class Application + fun main(args: Array) { SpringApplication.run(Application::class.java, *args) } diff --git a/src/main/kotlin/com/epam/brn/config/AwsConfig.kt b/src/main/kotlin/com/epam/brn/config/AwsConfig.kt index 76256d956..ca463aae5 100644 --- a/src/main/kotlin/com/epam/brn/config/AwsConfig.kt +++ b/src/main/kotlin/com/epam/brn/config/AwsConfig.kt @@ -61,6 +61,7 @@ class AwsConfig( val baseFileUrl: String = "" fun instant(): OffsetDateTime = Instant.now().atOffset(ZoneOffset.UTC) + fun uuid(): String = UUID.randomUUID().toString() private lateinit var accessKeyId: String diff --git a/src/main/kotlin/com/epam/brn/config/SwaggerConfig.kt b/src/main/kotlin/com/epam/brn/config/SwaggerConfig.kt index 538b329ae..e75ab69ff 100644 --- a/src/main/kotlin/com/epam/brn/config/SwaggerConfig.kt +++ b/src/main/kotlin/com/epam/brn/config/SwaggerConfig.kt @@ -28,18 +28,16 @@ class SwaggerConfig { fun rolesAllowedCustomizer(): OperationCustomizer? = OperationCustomizer { operation, handlerMethod -> var allowedRoles: Array? = null var rolesAllowedAnnotation = handlerMethod.getMethodAnnotation(RolesAllowed::class.java) - if (rolesAllowedAnnotation != null) { + if (rolesAllowedAnnotation != null) allowedRoles = rolesAllowedAnnotation.value - } else { + else rolesAllowedAnnotation = handlerMethod.method.declaringClass.getAnnotation(RolesAllowed::class.java) - if (rolesAllowedAnnotation != null) - allowedRoles = rolesAllowedAnnotation.value - } + if (rolesAllowedAnnotation != null) allowedRoles = rolesAllowedAnnotation.value val sb = StringBuilder("Roles: ") if (allowedRoles != null) sb.append("**${allowedRoles.joinToString(",")}**") - else + else sb.append("**PUBLIC**") operation.description?.let { diff --git a/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt b/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt index c4246060a..71691088a 100644 --- a/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt +++ b/src/main/kotlin/com/epam/brn/config/UserDetailControllerConfig.kt @@ -6,5 +6,5 @@ import org.springframework.context.annotation.Configuration @Configuration class UserDetailControllerConfig( @Value("\${brn.user.analytics.use.new.version}") - val isUseNewAnalyticsService: Boolean + val isUseNewAnalyticsService: Boolean, ) diff --git a/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt b/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt index 0a596b211..ddb54eb06 100644 --- a/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt +++ b/src/main/kotlin/com/epam/brn/controller/UserDetailController.kt @@ -39,7 +39,7 @@ class UserDetailController( private val doctorService: DoctorService, private val config: UserDetailControllerConfig, private val userAnalyticsService: UserAnalyticsService, - private val userAnalyticsServiceV1: UserAnalyticsServiceV1 + private val userAnalyticsServiceV1: UserAnalyticsServiceV1, ) { @GetMapping @Operation(summary = "Get all users with/without analytic data") @@ -49,7 +49,12 @@ class UserDetailController( @RequestParam("role", defaultValue = "USER") role: String, @PageableDefault pageable: Pageable, ): ResponseEntity { - val users = if (withAnalytics) if (config.isUseNewAnalyticsService) userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role) else userAnalyticsService.getUsersWithAnalytics(pageable, role) + val users = + if (withAnalytics) + if (config.isUseNewAnalyticsService) + userAnalyticsServiceV1.getUsersWithAnalytics(pageable, role) + else + userAnalyticsService.getUsersWithAnalytics(pageable, role) else userAccountService.getUsers(pageable, role) return ResponseEntity.ok().body(BrnResponse(data = users)) diff --git a/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt b/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt index 44c522496..d07385bb3 100644 --- a/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt +++ b/src/main/kotlin/com/epam/brn/job/UserAnalyticsJob.kt @@ -10,9 +10,10 @@ import org.springframework.transaction.annotation.Transactional @Component @ConditionalOnProperty(name = ["brn.user.analytics.job.enabled"], havingValue = "true") class UserAnalyticsJob( - private val jdbcTemplate: JdbcTemplate + private val jdbcTemplate: JdbcTemplate, ) { private val log = logger() + @Scheduled(cron = "@midnight") @Transactional fun fillUserAnalytics() { @@ -39,12 +40,12 @@ private const val FILL_USER_ANALYTICS_SQL: String = """ FROM study_history s1 WHERE s1.user_id = s.user_id AND s1.start_time between date_trunc('month', current_date) - AND date_trunc('month', current_date) + interval '1 month - 1 microsecond'), + AND current_date), r.name FROM study_history s, user_roles ur, role r WHERE s.user_id = ur.user_id - AND ur.role_id = r.id + AND ur.role_id = r.id GROUP BY s.user_id, r.name; """ diff --git a/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt b/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt index d34d92f2a..13b2c7007 100644 --- a/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt +++ b/src/main/kotlin/com/epam/brn/model/UserAnalytics.kt @@ -15,23 +15,33 @@ class UserAnalytics( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, - val userId: Long, - val firstDone: LocalDateTime?, - val lastDone: LocalDateTime?, - val spentTime: Long?, - val doneExercises: Int?, - val studyDays: Int?, - @Column(name = "role_name") - val roleName: String + val roleName: String, ) { - override fun toString(): String { - return "UserAnalytics(id=$id, userId=$userId, firstDone=$firstDone, lastDone=$lastDone, spentTime=$spentTime, doneExercises=$doneExercises, studyDays=$studyDays, roleName='$roleName')" + override fun toString(): String = + "UserAnalytics(id=$id, userId=$userId, firstDone=$firstDone, lastDone=$lastDone, spentTime=$spentTime, doneExercises=$doneExercises, studyDays=$studyDays, roleName='$roleName')" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserAnalytics + + if (id != other.id) return false + if (userId != other.userId) return false + + return true + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + userId.hashCode() + return result } } diff --git a/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt b/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt index 57d82ec25..d059d4951 100644 --- a/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt +++ b/src/main/kotlin/com/epam/brn/repo/UserAnalyticsRepository.kt @@ -25,7 +25,10 @@ interface UserAnalyticsRepository : JpaRepository { from UserAnalytics a join UserAccount u on a.userId = u.id where a.roleName=:roleName - """ + """, ) - fun getUserAnalytics(pageable: Pageable, roleName: String): List + fun getUserAnalytics( + pageable: Pageable, + roleName: String, + ): List } diff --git a/src/main/kotlin/com/epam/brn/service/TaskService.kt b/src/main/kotlin/com/epam/brn/service/TaskService.kt index ae99b703a..3ee75a455 100644 --- a/src/main/kotlin/com/epam/brn/service/TaskService.kt +++ b/src/main/kotlin/com/epam/brn/service/TaskService.kt @@ -91,8 +91,9 @@ class TaskService( private fun processAnswerOptions(task: Task) { task.answerOptions .forEach { resource -> - if (!resource.pictureFileUrl.isNullOrEmpty()) + if (!resource.pictureFileUrl.isNullOrEmpty()) { resource.pictureFileUrl = cloudService.baseFileUrl() + "/" + resource.pictureFileUrl + } } } diff --git a/src/main/kotlin/com/epam/brn/service/UserAccountService.kt b/src/main/kotlin/com/epam/brn/service/UserAccountService.kt index b2efc386a..1544fef9f 100644 --- a/src/main/kotlin/com/epam/brn/service/UserAccountService.kt +++ b/src/main/kotlin/com/epam/brn/service/UserAccountService.kt @@ -9,40 +9,57 @@ import org.springframework.data.domain.Pageable interface UserAccountService { fun findUserByEmail(email: String): UserAccountDto + fun createUser(firebaseUserRecord: UserRecord): UserAccountDto fun getCurrentUser(): UserAccount + fun findUserById(id: Long): UserAccount + fun getCurrentUserId(): Long + fun getCurrentUserRoles(): Set + fun getCurrentUserDto(): UserAccountDto + fun findUserDtoById(id: Long): UserAccountDto + fun findUserDtoByUuid(uuid: String): UserAccountDto? fun getUsers( pageable: Pageable, role: String, ): List + fun updateAvatarForCurrentUser(avatarUrl: String): UserAccountDto + fun updateCurrentUser(userChangeRequest: UserAccountChangeRequest): UserAccountDto fun addHeadphonesToUser( userId: Long, headphonesDto: HeadphonesDto, ): HeadphonesDto + fun addHeadphonesToCurrentUser(headphones: HeadphonesDto): HeadphonesDto + fun deleteHeadphonesForCurrentUser(headphonesId: Long) + fun getAllHeadphonesForUser(userId: Long): Set + fun getAllHeadphonesForCurrentUser(): Set fun updateDoctorForPatient( userId: Long, doctorId: Long, ): UserAccount + fun removeDoctorFromPatient(userId: Long): UserAccount + fun getPatientsForDoctor(doctorId: Long): List fun markVisitForCurrentUser() + fun deleteAutoTestUsers(): Long + fun deleteAutoTestUserByEmail(email: String): Long } diff --git a/src/main/kotlin/com/epam/brn/service/UserAnalyticsService.kt b/src/main/kotlin/com/epam/brn/service/UserAnalyticsService.kt index ec07fda1c..b54737c78 100644 --- a/src/main/kotlin/com/epam/brn/service/UserAnalyticsService.kt +++ b/src/main/kotlin/com/epam/brn/service/UserAnalyticsService.kt @@ -10,10 +10,12 @@ interface UserAnalyticsService { pageable: Pageable, role: String, ): List + fun prepareAudioStreamForUser( exerciseId: Long, audioFileMetaData: AudioFileMetaData, ): InputStream + fun prepareAudioFileMetaData( exerciseId: Long, audioFileMetaData: AudioFileMetaData, diff --git a/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt b/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt index 8a5138d7c..2f1ebca5a 100644 --- a/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt +++ b/src/main/kotlin/com/epam/brn/service/UserAnalyticsServiceV1.kt @@ -4,5 +4,8 @@ import com.epam.brn.dto.response.UserWithAnalyticsResponse import org.springframework.data.domain.Pageable interface UserAnalyticsServiceV1 { - fun getUsersWithAnalytics(pageable: Pageable, role: String): List + fun getUsersWithAnalytics( + pageable: Pageable, + role: String, + ): List } diff --git a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt index 40f30a08d..d9a98c02f 100644 --- a/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt +++ b/src/main/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1Impl.kt @@ -9,25 +9,27 @@ import kotlin.time.DurationUnit import kotlin.time.toDuration @Service -class UserAnalyticsServiceV1Impl(private val userAnalyticsRepository: UserAnalyticsRepository) : - UserAnalyticsServiceV1 { - - override fun getUsersWithAnalytics(pageable: Pageable, role: String): List = - userAnalyticsRepository.getUserAnalytics(pageable, role).map { - UserWithAnalyticsResponse( - id = it.id, - userId = it.userId, - name = it.fullName, - active = it.active, - email = it.email, - bornYear = it.bornYear, - gender = it.gender, - firstDone = it.firstDone, - lastDone = it.lastDone, - lastVisit = it.lastVisit, - doneExercises = it.doneExercises, - spentTime = it.spentTime.toDuration(DurationUnit.SECONDS), - studyDaysInCurrentMonth = it.studyDays - ) - } +class UserAnalyticsServiceV1Impl( + private val userAnalyticsRepository: UserAnalyticsRepository, +) : UserAnalyticsServiceV1 { + override fun getUsersWithAnalytics( + pageable: Pageable, + role: String, + ): List = userAnalyticsRepository.getUserAnalytics(pageable, role).map { + UserWithAnalyticsResponse( + id = it.id, + userId = it.userId, + name = it.fullName, + active = it.active, + email = it.email, + bornYear = it.bornYear, + gender = it.gender, + firstDone = it.firstDone, + lastDone = it.lastDone, + lastVisit = it.lastVisit, + doneExercises = it.doneExercises, + spentTime = it.spentTime.toDuration(DurationUnit.SECONDS), + studyDaysInCurrentMonth = it.studyDays, + ) + } } diff --git a/src/main/resources/firebase-brainupspb-dev.json b/src/main/resources/firebase-brainupspb-dev.json index cc4892ab0..bdd92f3ff 100644 --- a/src/main/resources/firebase-brainupspb-dev.json +++ b/src/main/resources/firebase-brainupspb-dev.json @@ -1,8 +1,8 @@ { "type": "service_account", "project_id": "brainup-spb-dev-bb6d0", - "private_key_id": "askElena https://t.me/ElenaBrainUp", - "private_key": "askElena https://t.me/ElenaBrainUp", + "private_key_id": "3fcb0863d269acb10f8b752a6f62ff92661b24dd", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCMNC0fT4tU/pxs\na2fVryZrNj303/LG9KpheyHi1v6cMS32wPx0BQVlwGhdmOyPiEbG6D/b80DLG+jY\nCX0ANQpIM5jCGQUTBwYxkyNVX1JrFkZa2pWxOTN4M4tqghJMfCUEcKO4dMY0qFiO\nBuhs+2MJixro1XDdt5QQI3tmcHZ+JKWj/WVl6vt+f7VJDGHGhd+VZpr2fFFQltN9\nmic1SxpmVUp8PGO4j4ZDUy4gYq3bS8G0xd61Wwrha8/o2nhSNzx169bkdB78wbBw\nIH3d92B1hs8hzC0FrwpjrZXA9BKt8TRzQl+1X93apxv8v9cCvDB1D44Uotpjc1LI\n9cA03O1lAgMBAAECggEACmuqWuRnn7aY27oT6p7tBRTv/+5RNhZjP4GOYDTIkcVZ\n/DgVy23AG/L0DtjZ5EUltYz3ihqHW1FEuiHiZ4yJqhojpXGEnghsnmyoOKUwFAOA\n0Snj7FsrBykVmKH4SDUF71GL6Z7MSIempU/AILGCh+gN5PZQQDpvx89LUa7UL8Xf\nW11D1eC7O2JQbIDJNCm7iWXy327wn7/+CZR468WR1Bf5Y8yvyN5E6rI9C7llUbM0\nPgSSNz9iVSqG0RPwIVws1As3wjVMpNOtGSjhRuVKcGxcrOAufXg1lYPabFSKpLNv\nKP4gc+2HKZXZJpq7uvk/TGLZFeWXGYFw3tK0dmZnwQKBgQDAfscW7A9nD2X7f4Li\nuKYK9hCFsz1gW8sjZ/IVmhYB2APr68cKUNvqBpZdwQq+cwBFXNYmCJoinyi0q4HZ\ni7FlOXnhu6an4w4cXFkNYWjMd/2Y3Gzsl2m5PQ1BGXRwKXgjQbcXEcw16lddVQiW\nYdr8+yB/dSqzRjf13F5lc0yyUQKBgQC6dR32b/KKfq5PvSzoe+4JVELH4VgPWZDI\nppQ6lks+NCRonUcsppu1adKIelWD7OsnWVALTWKyTrmsa4DQh8FFih7TFKut9lCD\nsD0/M2v6isOmJmyU6A8GHZBN6uCMpDZu+8OnIbZJHzDbEGnR6JEyVt8OKdBKDxDg\nxe6v/w+Q1QKBgEDdd7EdssMDyVXKTgygNDOVX1PuZkxGIlm3+TeWSLwuUoP5W4T6\nYmCl/51wI3KxxfGZqv/9/hKUl17qPENWc1ys1YlfdnU0adjctZVwsaPU+zu7a2j+\nTL4C+KhrL3VsQ/N9fXjkom+4m9/ze4VRTD3bUcQhc1Yd31WWAKknBT1BAoGBALY6\nE+hvjEkSeeVwa11jEUaI1SNn9po53KhdNOz1SeAnMZYUcCURR4hLPfkoJj9i4od4\nYCRLozPEgO4juqcSpi8CSBHfV9ISsqmhKpqD9PnNeFz/nIsDKPu/FPMxo/eP3asl\n3xeOeQqJs0PFjMYbVxwtjp3W+7wFcWdbymSzEFFpAoGARqTJX+HBvxMC7mVISdSy\nP7cpJhchnkVyauXEuNHhqpTAC1prQ2XwemzIAABY62cWpBaKhbZ2IyRtzmcZP9EC\n4tYYlj2bMP/+8qAyNTZuCZXUj860a0pX185Y0dse3h5G2G23SxK8bckpue2V22bv\nMiAsU6pB01pKfeJ6x3Kok/w=\n-----END PRIVATE KEY-----\n", "client_email": "firebase-adminsdk-a3xzg@brainup-spb-dev-bb6d0.iam.gserviceaccount.com", "client_id": "118039374628498438928", "auth_uri": "https://accounts.google.com/o/oauth2/auth", diff --git a/src/test/kotlin/com/epam/brn/integration/UserAnalyticsComparisonIT.kt b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsComparisonIT.kt new file mode 100644 index 000000000..b780d14b8 --- /dev/null +++ b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsComparisonIT.kt @@ -0,0 +1,86 @@ +package com.epam.brn.integration + +import com.epam.brn.config.AwsConfig +import com.epam.brn.job.UserAnalyticsJob +import com.epam.brn.model.UserAnalytics +import com.epam.brn.repo.UserAnalyticsRepository +import com.epam.brn.service.ExerciseService +import com.epam.brn.service.TimeService +import com.epam.brn.service.UrlConversionService +import com.epam.brn.service.WordsService +import com.epam.brn.service.YandexSpeechKitService +import com.epam.brn.service.cloud.AwsCloudService +import com.epam.brn.service.impl.HeadphonesServiceImpl +import com.epam.brn.service.impl.RoleServiceImpl +import com.epam.brn.service.impl.UserAccountServiceImpl +import com.epam.brn.service.impl.UserAnalyticsServiceImpl +import com.epam.brn.service.impl.UserAnalyticsServiceV1Impl +import com.epam.brn.service.statistics.impl.UserDayStatisticsService +import com.epam.brn.service.statistics.progress.status.impl.StudyHistoriesProgressStatusManager +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Import +import org.springframework.data.domain.Pageable +import org.springframework.test.context.jdbc.Sql +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +@DataJpaTest +@Tag("integration-test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Import( + *[ + UserAnalyticsJob::class, UserAnalyticsServiceImpl::class, UserAnalyticsServiceV1Impl::class, + UserDayStatisticsService::class, UserAccountServiceImpl::class, RoleServiceImpl::class, HeadphonesServiceImpl::class, + TimeService::class, StudyHistoriesProgressStatusManager::class, YandexSpeechKitService::class, YandexSpeechKitService::class, + WordsService::class, ExerciseService::class, UrlConversionService::class, AwsCloudService::class, + AwsConfig::class, + ], +) +class UserAnalyticsComparisonIT { + @Autowired + lateinit var userAnalyticsJob: UserAnalyticsJob + + @Autowired + lateinit var userAnalyticsRepository: UserAnalyticsRepository + + @Autowired + lateinit var userAnalyticsServiceImpl: UserAnalyticsServiceImpl + + @Autowired + lateinit var userAnalyticsServiceV1Impl: UserAnalyticsServiceV1Impl + + @Test + @Sql("/db/init-study-history.sql") + fun `test filling user analytics job`() { + // GIVEN + val now = LocalDateTime.now() + val firstDone = now.minusHours(1L).truncatedTo(ChronoUnit.SECONDS) + val lastDone = now.plusHours(1L).truncatedTo(ChronoUnit.SECONDS) + val roleUser = "USER" + val roleAdmin = "ADMIN" + + val userAnalytics1 = UserAnalytics(1, 1, firstDone, lastDone, 50, 2, 1, roleAdmin) + val userAnalytics2 = UserAnalytics(2, 1, firstDone, lastDone, 50, 2, 1, roleUser) + val userAnalytics3 = UserAnalytics(3, 2, firstDone, lastDone, 36, 2, 1, roleUser) + + // WHEN + userAnalyticsJob.fillUserAnalytics() + val usersWithAnalytics = userAnalyticsServiceImpl.getUsersWithAnalytics(Pageable.unpaged(), "USER") + val usersWithAnalytics1 = userAnalyticsServiceV1Impl.getUsersWithAnalytics(Pageable.unpaged(), "USER") + val userAnalyticsList = userAnalyticsRepository.findAll() + + // THEN + userAnalyticsList.size shouldBe 3 + userAnalyticsList shouldContainAll listOf(userAnalytics1, userAnalytics2, userAnalytics3) + usersWithAnalytics.size shouldBe usersWithAnalytics1.size + usersWithAnalytics shouldContainAll usersWithAnalytics1 + } +} diff --git a/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt index 9d53f274b..86268272a 100644 --- a/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt +++ b/src/test/kotlin/com/epam/brn/integration/UserAnalyticsJobIT.kt @@ -60,7 +60,7 @@ class UserAnalyticsJobIT : BaseIT() { it.lastDone shouldBe secondStudyHistory.startTime it.spentTime shouldBe (firstStudyHistory.spentTimeInSeconds ?: 0L) + (secondStudyHistory.spentTimeInSeconds ?: 0L) it.doneExercises shouldBe 2 - it.studyDays shouldBe 1 + it.studyDays shouldBe 0 it.roleName shouldBe roleName } } diff --git a/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt b/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt index 3466c3a39..e06499eaa 100644 --- a/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt +++ b/src/test/kotlin/com/epam/brn/service/impl/UserAnalyticsServiceV1ImplTest.kt @@ -24,7 +24,6 @@ private const val ONE_LONG = 1L @ExtendWith(MockKExtension::class) class UserAnalyticsServiceV1ImplTest { - @InjectMockKs lateinit var service: UserAnalyticsServiceV1Impl diff --git a/src/test/resources/db/init-study-history.sql b/src/test/resources/db/init-study-history.sql new file mode 100644 index 000000000..68c3dbc8a --- /dev/null +++ b/src/test/resources/db/init-study-history.sql @@ -0,0 +1,35 @@ +INSERT INTO public.role (id, name) VALUES +(1, 'ADMIN'), +(2, 'USER'); + +INSERT INTO public.user_account (id, active, avatar, born_year, changed, changed_by, created, description, email, full_name, gender, password, photo, user_id, doctor_id, is_firebase_error, last_visit) VALUES +(1, true, '1', 1990, '2022-11-23 11:18:09.173318', 'admin@admin.com', '2021-07-30 08:13:15.587000', null, 'admin@admin.com', 'Elena Mosh', 'MALE', '$2a$10$0pmieR1fAW3IZO3xFbVjj.c9i2jhhGf/rFdLkG3A0lH89SpW1Mxdu', null, 'f89e5760-0caf-4a95-9810-cd6aa4a8261e', null, false, '2024-12-22 11:52:53.535561'), +(2, true, '13', 2024, '2024-03-29 16:51:09.788167', 'default@default.ru', '2021-07-30 08:13:15.673000', null, 'default@default.ru', 'AAA BBB', 'FEMALE', '$2a$10$P0oKm.pXyft/do/xMmR4f.q7a8MjTwCTrncOmM3khzuJoFOlhVtT6', null, '5cbe5936-9201-4f27-b148-273d6e1691b3', null, false, '2025-01-10 02:15:38.984504'); +INSERT INTO public.user_roles (user_id, role_id) VALUES +(1, 1), +(1, 2), +(2, 2); + +INSERT INTO public.exercise_group (id, code, description, locale, name) VALUES +(2, 'SPEECH_RU_RU', 'Речевые упражнения', 'ru-ru', 'Речевые упражнения (готовы для занятий)'); + +INSERT INTO public.series (id, description, level, name, type, exercise_group_id) VALUES +(1, 'Распознавание слов', 1, 'Слова', 'SINGLE_SIMPLE_WORDS', 2), +(17, 'Слова по методическому пособию Инны Васильевны Королевой Учусь слушать и говорить', 8, 'Слова Королёвой', 'SINGLE_WORDS_KOROLEVA', 2); + +INSERT INTO public.sub_group (id, code, description, level, name, exercise_series_id, with_pictures) VALUES +(1, 'family', 'Слова про семью', 1, 'Семья', 1, false), +(115, 'koroleva_words_first_1', '1я группа слов: по одному', 1, '1 слово из 2..4 (1)', 17, false), +(25, 'music', 'Музыка', 25, 'Музыка', 1, false); + +INSERT INTO public.exercise (id, active, changed_by, changed_when, level, name, noise_level, noise_url, template, sub_group_id, play_words_count, words_columns) VALUES +(1907, true, 'InitialDataLoader', '2022-01-23 19:33:18.747434', 3, '1я группа', 0, '', '', 115, 1, 2), +(1, true, 'InitialDataLoader', '2021-07-30 08:13:19.820000', 1, 'Семья', 0, '', '', 1, 1, 3), +(2, true, 'InitialDataLoader', '2021-07-30 08:13:19.884000', 2, 'Семья', 0, '', '', 1, 1, 3), +(689, true, 'InitialDataLoader', '2021-07-30 08:14:20.473000', 1, 'Музыка', 0, '', '', 25, 1, 3); + +INSERT INTO public.study_history (id, end_time, execution_seconds, repetition_index, replays_count, right_answers_index, start_time, tasks_count, wrong_answers, exercise_id, user_id, spent_time_in_seconds) VALUES +(11723, now() - interval '1 week' + interval '1 hour', 12, 0, 0, 1, now() - interval '1 week' - interval '1 hour', 6, 0, 1907, 2, 15), +(4320, now() - interval '1 week' + interval '1 hour', 23, 0.1, 1, 1, now() - interval '1 week' - interval '1 hour', 9, 0, 1, 1, 27), +(4321, now() - interval '1 week' + interval '1 hour', 19, 0.1, 1, 1, now() - interval '1 week' - interval '1 hour', 9, 0, 2, 1, 23), +(8122, now() - interval '1 week' + interval '1 hour', 16, 0, 0, 1, now() - interval '1 week' - interval '1 hour', 9, 0, 689, 2, 21); \ No newline at end of file