diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt index 29c59d2..d98f1c1 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepository.kt @@ -18,33 +18,8 @@ package org.radarbase.datadashboard.api.domain -import jakarta.inject.Provider -import jakarta.persistence.EntityManager -import jakarta.ws.rs.core.Context import org.radarbase.datadashboard.api.domain.model.Observation -import org.radarbase.jersey.hibernate.HibernateRepository -import org.slf4j.LoggerFactory -class ObservationRepository( - @Context em: Provider, -) : HibernateRepository(em) { - - fun getObservations(projectId: String, subjectId: String, topicId: String): List { - logger.debug("Get observations in topic {} of subject {} in project {}", topicId, subjectId, projectId) - - return transact { - createQuery( - "SELECT o FROM Observation o WHERE o.project = :projectId AND o.subject = :subjectId AND o.topic = :topicId ORDER BY o.observationTime DESC", - Observation::class.java, - ).apply { - setParameter("projectId", projectId) - setParameter("subjectId", subjectId) - setParameter("topicId", topicId) - }.resultList - } - } - - companion object { - private val logger = LoggerFactory.getLogger(ObservationRepository::class.java) - } +interface ObservationRepository { + suspend fun getObservations(projectId: String, subjectId: String, topicId: String): List } diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepositoryImpl.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepositoryImpl.kt new file mode 100644 index 0000000..68f49cb --- /dev/null +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/ObservationRepositoryImpl.kt @@ -0,0 +1,52 @@ +/* + * + * * Copyright 2024 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.radarbase.datadashboard.api.domain + +import jakarta.inject.Provider +import jakarta.persistence.EntityManager +import jakarta.ws.rs.core.Context +import org.radarbase.datadashboard.api.domain.model.Observation +import org.radarbase.jersey.hibernate.HibernateRepository +import org.radarbase.jersey.service.AsyncCoroutineService +import org.slf4j.LoggerFactory + +class ObservationRepositoryImpl( + @Context em: Provider, + @Context asyncService: AsyncCoroutineService, +) : HibernateRepository(em, asyncService), ObservationRepository { + + override suspend fun getObservations(projectId: String, subjectId: String, topicId: String): List { + logger.debug("Get observations in topic {} of subject {} in project {}", topicId, subjectId, projectId) + + return transact { + createQuery( + "SELECT o FROM Observation o WHERE o.project = :projectId AND o.subject = :subjectId AND o.topic = :topicId ORDER BY o.observationTime DESC", + Observation::class.java, + ).apply { + setParameter("projectId", projectId) + setParameter("subjectId", subjectId) + setParameter("topicId", topicId) + }.resultList + } + } + + companion object { + private val logger = LoggerFactory.getLogger(ObservationRepositoryImpl::class.java) + } +} diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashboardApiEnhancer.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashboardApiEnhancer.kt index 6e806b4..3f0949c 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashboardApiEnhancer.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashboardApiEnhancer.kt @@ -22,7 +22,9 @@ import jakarta.inject.Singleton import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.datadashboard.api.config.DashboardApiConfig import org.radarbase.datadashboard.api.domain.ObservationRepository +import org.radarbase.datadashboard.api.domain.ObservationRepositoryImpl import org.radarbase.datadashboard.api.service.ObservationService +import org.radarbase.datadashboard.api.service.ObservationServiceImpl import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters @@ -44,11 +46,11 @@ class DashboardApiEnhancer( bind(config) .to(DashboardApiConfig::class.java) - bind(ObservationService::class.java) + bind(ObservationServiceImpl::class.java) .to(ObservationService::class.java) .`in`(Singleton::class.java) - bind(ObservationRepository::class.java) + bind(ObservationRepositoryImpl::class.java) .to(ObservationRepository::class.java) .`in`(Singleton::class.java) } diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt index 1bca0c2..2ca53cc 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/resource/ObservationResource.kt @@ -19,17 +19,20 @@ package org.radarbase.datadashboard.api.resource import jakarta.annotation.Resource +import jakarta.inject.Singleton import jakarta.ws.rs.Consumes import jakarta.ws.rs.GET import jakarta.ws.rs.Path import jakarta.ws.rs.PathParam import jakarta.ws.rs.Produces +import jakarta.ws.rs.container.AsyncResponse +import jakarta.ws.rs.container.Suspended import jakarta.ws.rs.core.Context import org.radarbase.auth.authorization.Permission -import org.radarbase.datadashboard.api.api.ObservationListDto import org.radarbase.datadashboard.api.service.ObservationService import org.radarbase.jersey.auth.Authenticated import org.radarbase.jersey.auth.NeedsPermission +import org.radarbase.jersey.service.AsyncCoroutineService import org.slf4j.LoggerFactory @Path("project/{projectId}/subject/{subjectId}/topic/{topicId}") @@ -37,8 +40,10 @@ import org.slf4j.LoggerFactory @Produces("application/json") @Consumes("application/json") @Authenticated +@Singleton class ObservationResource( @Context private val observationService: ObservationService, + @Context private val asyncService: AsyncCoroutineService, ) { @GET @Path("observations") @@ -47,8 +52,9 @@ class ObservationResource( @PathParam("projectId") projectId: String, @PathParam("subjectId") subjectId: String, @PathParam("topicId") topicId: String, - ): ObservationListDto { - return observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) + @Suspended asyncResponse: AsyncResponse, + ) = asyncService.runAsCoroutine(asyncResponse) { + observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) } companion object { diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt index e2bc479..d9cf38e 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationService.kt @@ -18,18 +18,8 @@ package org.radarbase.datadashboard.api.service -import jakarta.ws.rs.core.Context import org.radarbase.datadashboard.api.api.ObservationListDto -import org.radarbase.datadashboard.api.domain.ObservationRepository -import org.radarbase.datadashboard.api.domain.mapper.toDto -class ObservationService( - @Context private val observationRepository: ObservationRepository, -) { - fun getObservations(projectId: String, subjectId: String, topicId: String): ObservationListDto { - val result = this.observationRepository.getObservations(projectId = projectId, topicId = topicId, subjectId = subjectId) - return ObservationListDto( - result.map { it.toDto() }, - ) - } +interface ObservationService { + suspend fun getObservations(projectId: String, subjectId: String, topicId: String): ObservationListDto } diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationServiceImpl.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationServiceImpl.kt new file mode 100644 index 0000000..f728ac3 --- /dev/null +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/service/ObservationServiceImpl.kt @@ -0,0 +1,35 @@ +/* + * + * * Copyright 2024 The Hyve + * * + * * Licensed under the Apache License, Version 2.0 (the "License"); + * * you may not use this file except in compliance with the License. + * * You may obtain a copy of the License at + * * + * * http://www.apache.org/licenses/LICENSE-2.0 + * * + * * Unless required by applicable law or agreed to in writing, software + * * distributed under the License is distributed on an "AS IS" BASIS, + * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * * See the License for the specific language governing permissions and + * * limitations under the License. + * + */ + +package org.radarbase.datadashboard.api.service + +import jakarta.ws.rs.core.Context +import org.radarbase.datadashboard.api.api.ObservationListDto +import org.radarbase.datadashboard.api.domain.ObservationRepository +import org.radarbase.datadashboard.api.domain.mapper.toDto + +class ObservationServiceImpl( + @Context private val observationRepository: ObservationRepository, +) : ObservationService { + override suspend fun getObservations(projectId: String, subjectId: String, topicId: String): ObservationListDto { + val result = this.observationRepository.getObservations(projectId = projectId, topicId = topicId, subjectId = subjectId) + return ObservationListDto( + result.map { it.toDto() }, + ) + } +} diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt index a253600..1e0079f 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/DashboardIntegrationTest.kt @@ -30,28 +30,29 @@ import org.glassfish.jersey.test.spi.TestContainerFactory import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.radarbase.auth.authorization.AuthorizationOracle import org.radarbase.datadashboard.api.config.DashboardApiConfig -import org.radarbase.datadashboard.api.resource.ObservationResource +import org.radarbase.datadashboard.api.enhancer.DashBoardApiEnhancerFactory import org.radarbase.jersey.auth.AuthValidator import org.radarbase.jersey.auth.disabled.DisabledAuthValidator +import org.radarbase.jersey.auth.disabled.DisabledAuthorizationOracle import org.radarbase.jersey.config.ConfigLoader -// These tests are ok, but the injection of the DisabledAuthValidator is not correct. -// I do not know how to fix it. +// These tests are not yet working because mocking/stubbing token validation is not yet working. class DashboardIntegrationTest : JerseyTest() { - lateinit var disabledAuthValidator: DisabledAuthValidator - override fun configure(): ResourceConfig { val config: DashboardApiConfig = ConfigLoader.loadConfig("src/test/resources/dashboard_test.yml", emptyArray()) val resourceConfig = ConfigLoader.loadResources(config.service.resourceConfig, config) - disabledAuthValidator = DisabledAuthValidator(config.auth) + resourceConfig.register(DashBoardApiEnhancerFactory::class) + val disabledAuthorizationOracle = DisabledAuthorizationOracle() + val disabledAuthValidator = DisabledAuthValidator(config.auth) resourceConfig.register(object : AbstractBinder() { override fun configure() { + bind(disabledAuthorizationOracle).to(AuthorizationOracle::class.java).ranked(1) bind(disabledAuthValidator).to(AuthValidator::class.java).ranked(1) } }) - resourceConfig.register(ObservationResource::class.java) return resourceConfig } @@ -74,18 +75,24 @@ class DashboardIntegrationTest : JerseyTest() { @Disabled("Token integration tests do not work yet.") @Test fun testGetObservationsNoToken() { - val response = target("project/project-1/subject/sub-1/topic/phone_battery_level/observations").request().get() - Assertions.assertEquals(401, response.status) + target("project/project-1/subject/sub-1/topic/phone_battery_level/observations") + .request() + .get() + .use { response -> + Assertions.assertEquals(401, response.status) + } } @Disabled("Token integration tests do not work yet.") @Test fun testGetObservationsWithToken() { - val response = target("project/project-1/subject/sub-1/topic/phone_battery_level/observations") + target("project/project-1/subject/sub-1/topic/phone_battery_level/observations") .request() .header(HttpHeaders.AUTHORIZATION, "Bearer " + "... encoded token ...") .get() - Assertions.assertEquals(200, response.status) + .use { response -> + Assertions.assertEquals(200, response.status) + } } // TODO add more tests that include the token validation. diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/mock/MockAsyncCoroutineService.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/mock/MockAsyncCoroutineService.kt new file mode 100644 index 0000000..6e8765f --- /dev/null +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/mock/MockAsyncCoroutineService.kt @@ -0,0 +1,32 @@ +package org.radarbase.upload.mock + +import jakarta.ws.rs.container.AsyncResponse +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import org.radarbase.jersey.service.AsyncCoroutineService +import kotlin.time.Duration + +class MockAsyncCoroutineService : AsyncCoroutineService { + override fun runAsCoroutine( + asyncResponse: AsyncResponse, + timeout: Duration, + block: suspend () -> T, + ) { + runBlocking { + val result = block() + asyncResponse.resume(result) + } + } + + override fun runBlocking(timeout: Duration, block: suspend () -> T): T = + kotlinx.coroutines.runBlocking { + block() + } + + override suspend fun runInRequestScope(block: () -> T): T = block() + + override suspend fun suspendInRequestScope(block: (CancellableContinuation) -> Unit): T = suspendCancellableCoroutine(block) + override suspend fun withContext(name: String, block: suspend () -> T): T { + TODO("Not yet implemented") + } +} diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt index d11d817..1a13f3c 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt @@ -19,6 +19,7 @@ package org.radarbase.datadashboard.api.resource import jakarta.ws.rs.core.Application +import kotlinx.coroutines.runBlocking import org.glassfish.hk2.utilities.binding.AbstractBinder import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.test.JerseyTest @@ -32,6 +33,8 @@ import org.radarbase.datadashboard.api.api.ObservationListDto import org.radarbase.datadashboard.api.domain.mapper.toDto import org.radarbase.datadashboard.api.domain.model.Observation import org.radarbase.datadashboard.api.service.ObservationService +import org.radarbase.jersey.service.AsyncCoroutineService +import org.radarbase.upload.mock.MockAsyncCoroutineService import java.time.ZonedDateTime class ObservationResourceTest : JerseyTest() { @@ -40,7 +43,6 @@ class ObservationResourceTest : JerseyTest() { lateinit var observationService: ObservationService private lateinit var observationListDto: ObservationListDto - private var observationId: Long = 1 private val projectId = "project-1" private val subjectId = "sub-1" private val topicId = "topic-1" @@ -54,6 +56,7 @@ class ObservationResourceTest : JerseyTest() { resourceConfig.register(object : AbstractBinder() { override fun configure() { bind(observationService).to(ObservationService::class.java) + bind(MockAsyncCoroutineService()).to(AsyncCoroutineService::class.java) } }) return resourceConfig @@ -70,32 +73,48 @@ class ObservationResourceTest : JerseyTest() { } @Test - fun testGetObservations() { + fun testGetObservations() = runBlocking { // Instruct the mock to return the fake observations when called. `when`(observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId)).thenReturn(observationListDto) // Make the call to the REST endpoint. - val response = target("project/project-1/subject/sub-1/topic/topic-1/observations").request().get() - // Expect the http response to be OK and the same as the expected DTO. - assertEquals(200, response.status) - assertEquals(observationListDto, response.readEntity(ObservationListDto::class.java)) + target("project/project-1/subject/sub-1/topic/topic-1/observations") + .request() + .get() + .use { response -> + // Expect the http response to be OK and the same as the expected DTO. + assertEquals(200, response.status) + assertEquals(observationListDto, response.readEntity(ObservationListDto::class.java)) + } } @Test - fun testGetObservations_failNoSubjectId() { - val response = target("project/project-1/subject//topic/topic-1/observations").request().get() - assertEquals(404, response.status) + fun testGetObservations_failNoSubjectId() = runBlocking { + target("project/project-1/subject//topic/topic-1/observations") + .request() + .get() + .use { response -> + assertEquals(404, response.status) + } } @Test - fun testGetObservations_failNoTopicId() { - val response = target("project/project-1/subject/sub-1/topic//observations").request().get() - assertEquals(404, response.status) + fun testGetObservations_failNoTopicId() = runBlocking { + target("project/project-1/subject/sub-1/topic//observations") + .request() + .get() + .use { response -> + assertEquals(404, response.status) + } } @Test - fun testGetObservations_failNoProjectId() { - val response = target("project//subject/sub-1/topic/topic-1/observations").request().get() - assertEquals(404, response.status) + fun testGetObservations_failNoProjectId() = runBlocking { + target("project//subject/sub-1/topic/topic-1/observations") + .request() + .get() + .use { response -> + assertEquals(404, response.status) + } } private fun createObservation(): Observation { diff --git a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceImplTest.kt similarity index 92% rename from data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt rename to data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceImplTest.kt index c91117f..16c6430 100644 --- a/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceImplTest.kt @@ -18,41 +18,42 @@ package org.radarbase.datadashboard.api.service +import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.mockito.Mock import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.radarbase.datadashboard.api.api.ObservationListDto -import org.radarbase.datadashboard.api.domain.ObservationRepository +import org.radarbase.datadashboard.api.domain.ObservationRepositoryImpl import org.radarbase.datadashboard.api.domain.mapper.toDto import org.radarbase.datadashboard.api.domain.model.Observation import java.time.ZonedDateTime -class ObservationServiceTest { +class ObservationServiceImplTest { // Create a Mockito mock of the ObservationRepository. This is instantiated in the init block. @Mock - private lateinit var observationRepository: ObservationRepository + private lateinit var observationRepository: ObservationRepositoryImpl private var observationId: Long = 1 private val projectId = "project-1" private val subjectId = "sub-1" private val topicId = "topic-1" - private val observationService: ObservationService + private val observationService: ObservationServiceImpl init { // Initialize all Mockito mocks. MockitoAnnotations.openMocks(this) - observationService = ObservationService(observationRepository) + observationService = ObservationServiceImpl(observationRepository) } /** This test does not test much (only whether the service calls the repository). * I made it mainly to document how to write a test with mocking. * */ @Test - fun test_getObservations1() { + fun test_getObservations1() = runBlocking { // Create some fake observations that are returned by the repository. // Each observation is linked to a Variable. val observations: List = listOf(createObservation(), createObservation(), createObservation(), createObservation()) diff --git a/data-dashboard-backend/src/test/resources/dashboard_test.yml b/data-dashboard-backend/src/test/resources/dashboard_test.yml index 3f11166..c3249aa 100644 --- a/data-dashboard-backend/src/test/resources/dashboard_test.yml +++ b/data-dashboard-backend/src/test/resources/dashboard_test.yml @@ -15,6 +15,7 @@ database: user: password: dialect: org.hibernate.dialect.H2Dialect + driver: org.h2.Driver liquibase: contexts: [dev] properties: