diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 21a4e0c..03d0bd8 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,8 +1,6 @@ #See: https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages name: Create and publish a Docker image -on: - release: - types: [ published ] +on: push env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} @@ -20,11 +18,17 @@ jobs: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Set environment variables + run: | + # Short name for current branch. For PRs, use target branch (base ref) + GIT_BRANCH=${GITHUB_BASE_REF:-${GITHUB_REF#refs/heads/}} + echo "GIT_BRANCH=$GIT_BRANCH" >> $GITHUB_ENV - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ env.GIT_BRANCH }} + tags: latest - name: Build and push Docker image uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f2fddaa..6bb4eb3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: types: [published] env: - DOCKER_IMAGE: radarbase/data-dashboard-backend + DOCKER_IMAGE: radarbase/radar-data-dashboard-backend jobs: # Build and push tagged release docker image @@ -23,7 +23,7 @@ jobs: id: docker_meta uses: docker/metadata-action@v4 with: - images: ${{ env.DOCKER_IMAGE }} + images: ${{ env.DOCKER_IMAGE }}-${{ env.GIT_BRANCH }} tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} diff --git a/README.md b/README.md index f6ac611..ff0ce91 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ the data from the RADAR-base kafka service.[]\ Data dashboard applications can use the APIs as follows. -`GET */subject/{subjectId}/topic/{topicId}/observations` +`GET */project/{projectId}/subject/{subjectId}/topic/{topicId}/observations` ## Installation @@ -43,8 +43,7 @@ The OAuth client for authorizer-app-backend should have the following properties ```properties client-id:data_dashboard_api client-secret:Confidential -grant-type:client_credentials +grant-type:authorization_code,refresh_token resources:res_DataDashboardAPI scope:MEASUREMENT.READ ``` - diff --git a/data-dashboard-backend/dev/dashboard.yml b/data-dashboard-backend/dev/dashboard.yml index 14e86c6..c2e0cc5 100644 --- a/data-dashboard-backend/dev/dashboard.yml +++ b/data-dashboard-backend/dev/dashboard.yml @@ -5,13 +5,13 @@ service: auth: managementPortal: - url: http://localhost:8080/managementportal + url: http://127.0.0.1:8080/managementportal clientId: data_dashboard_api clientSecret: data_dashboard_api_secret jwtResourceName: res_DataDashboardAPI database: - url: jdbc:postgresql://localhost:5432/data + url: jdbc:postgresql://127.0.0.1:5432/data user: radarbase password: radarbase dialect: org.hibernate.dialect.PostgreSQLDialect diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt index 3317c15..a461531 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationDto.kt @@ -20,8 +20,21 @@ package org.radarbase.datadashboard.api.api /** Single observation or data point. */ data class ObservationDto( - /** Unique observation ID. */ - val id: Long?, + + /** Unique identifier of project. */ + val project: String?, + + /** Unique identifier of study subject. */ + val subject: String?, + + /** Unique identifier of the data source. */ + val source: String?, + + /** Unique identifier of the kafka topic. */ + val topic: String?, + + /** Category of the observation (optional). */ + val category: String?, /** Date or date-time of the observation. */ val date: String?, diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableListDto.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationListDto.kt similarity index 90% rename from data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableListDto.kt rename to data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationListDto.kt index 761a397..85eb17b 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableListDto.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/ObservationListDto.kt @@ -19,6 +19,6 @@ package org.radarbase.datadashboard.api.api /** List of variables. */ -data class VariableListDto( - val variables: List, +data class ObservationListDto( + val observations: List, ) diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableDto.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableDto.kt deleted file mode 100644 index 7997a5f..0000000 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/api/VariableDto.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * - * * 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.api - -import org.radarbase.datadashboard.api.domain.model.Variable - -/** A variable, describing a type of observations. */ -data class VariableDto( - /** Unique ID. */ - val id: Long?, - /** Canonical name of the variable. */ - val name: String, - /** Data type of observations for this variable. */ - val type: String?, - /** Category to group variables with. */ - val category: String?, - /** Observations that are selected for this variable. */ - val observations: List?, - /** Date type of observations. */ - val dateType: Variable.DateType? = Variable.DateType.LOCAL_DATE, -) diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/config/DashboardServiceConfig.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/config/DashboardServiceConfig.kt index 4e08ba0..74149bb 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/config/DashboardServiceConfig.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/config/DashboardServiceConfig.kt @@ -40,4 +40,4 @@ data class DashboardServiceConfig( tokenExpiryTimeInMinutes = System.getenv("RADAR_DATA_DASHBOARD_TOKEN_EXPIRY_TIME_IN_MINUTES")?.toLong() ?: tokenExpiryTimeInMinutes, persistentTokenExpiryInMin = System.getenv("RADAR_DATA_DASHBOARD_PERSISTENT_TOKEN_EXPIRY_IN_MIN")?.toLong() ?: persistentTokenExpiryInMin, ) -} \ No newline at end of file +} 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 0a15ebd..5f82150 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 @@ -20,30 +20,27 @@ package org.radarbase.datadashboard.api.domain import jakarta.inject.Provider import jakarta.persistence.EntityManager -import jakarta.persistence.Tuple 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(topicId: String, subjectId: String): List> { - logger.debug("Get observations of topic {} and subject {}", topicId, subjectId) - val rows: List = transact { - createNativeQuery( - "SELECT * FROM ${topicId} o WHERE o.userId = '${subjectId}' ORDER BY o.timestamp DESC", - Tuple::class.java - ).resultList - } - // Create a list of maps from the rows. - return rows.map { row -> - (row as Tuple).elements.associate { - // Column name is the key, value is the database value. - it.alias to row.get(it.alias) - } + 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.date DESC", + Observation::class.java, + ).apply { + setParameter("projectId", projectId) + setParameter("subjectId", subjectId) + setParameter("topicId", topicId) + }.resultList } } diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt index 5086efd..c6b46d8 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/mapper/Extensions.kt @@ -19,38 +19,20 @@ package org.radarbase.datadashboard.api.domain.mapper import org.radarbase.datadashboard.api.api.ObservationDto -import org.radarbase.datadashboard.api.api.VariableDto import org.radarbase.datadashboard.api.domain.model.Observation -import org.radarbase.datadashboard.api.domain.model.Variable import java.time.Duration fun Observation.toDto(): ObservationDto = ObservationDto( - id = id, - date = date?.toString(), - period = if (date != null && endDate != null) { + project = project, + subject = subject, + source = source, + topic = topic, + category = category, + date = date.toString(), + period = if (endDate != null) { Duration.between(date, endDate).toString() } else { null }, value = valueNumeric ?: valueTextual, ) - -fun Map.Entry>.toDto(): VariableDto { - val (variable, observations) = this - return variable.toDto( - observations - .sortedBy { it.date } - .map { it.toDto() }, - ) -} - -fun Variable.toDtoWithoutObservations(): VariableDto = toDto(null) - -fun Variable.toDto(observations: List?): VariableDto = VariableDto( - id = id, - name = name, - type = type, - category = category, - observations = observations, - dateType = dateType, -) diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt index 2120053..c642f31 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Observation.kt @@ -18,54 +18,65 @@ package org.radarbase.datadashboard.api.domain.model -import jakarta.persistence.* +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table import java.time.ZonedDateTime import java.util.* @Entity @Table(name = "observation") -class Observation( - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(updatable = false, nullable = false) +data class Observation( + @Column(nullable = false) @Id - val id: Long?, - @Column(name = "subject_id") - val subjectId: String, - @ManyToOne - @JoinColumn(name = "variable_id") - val variable: Variable, - val date: ZonedDateTime?, + val project: String, + + @Column(nullable = false) + @Id + val subject: String, + + @Id + val source: String, + + @Column(nullable = false) + @Id + val topic: String, + + val category: String, + + @Column(nullable = false) + @Id + val variable: String, + + @Column(nullable = false) + @Id + val date: ZonedDateTime, + @Column(name = "end_date") val endDate: ZonedDateTime?, + @Column(name = "value_textual") val valueTextual: String?, + @Column(name = "value_numeric") val valueNumeric: Double?, + ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Observation - if (id != null && other.id != null) { - return id == other.id - } - - return subjectId == other.subjectId && + return subject == other.subject && + topic == other.topic && + category == other.category && variable == other.variable && date == other.date && endDate == other.endDate } - override fun hashCode(): Int = Objects.hash(subjectId, variable, date) - - override fun toString(): String = "Observation(" + - "id=$id, " + - "variable=$variable, " + - "date=$date, " + - "endDate=$endDate, " + - "valueTextual=${valueTextual.toPrintString()}, " + - "valueNumeric=$valueNumeric)" + override fun hashCode(): Int = Objects.hash(subject, variable, date) companion object { internal fun String?.toPrintString() = if (this != null) "'$this'" else "null" diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Variable.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Variable.kt deleted file mode 100644 index 104bcf8..0000000 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/domain/model/Variable.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * - * * 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.model - -import org.radarbase.datadashboard.api.domain.model.Observation.Companion.toPrintString -import jakarta.persistence.* - -@Entity -@Table(name = "variable") -class Variable( - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Id - val id: Long?, - val name: String, - val type: String?, - val category: String?, - @OneToMany(mappedBy = "variable", fetch = FetchType.LAZY) - val observations: List, - @Enumerated(EnumType.STRING) - @Column(name = "date_type") - val dateType: DateType? = DateType.LOCAL_DATE, -) { - enum class DateType { - LOCAL_DATE, ZONED_DATE, LOCAL_DATE_TIME, ZONED_DATE_TIME, - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Variable - - if (id != null && other.id != null) { - return id == other.id - } - - return name == other.name && - type == other.type && - category == other.category && - dateType == other.dateType - } - - override fun hashCode(): Int = name.hashCode() - - override fun toString(): String = "Variable(" + - "id=$id, " + - "name='$name', " + - "type=${type.toPrintString()}, " + - "category=${category.toPrintString()}, " + - "dateType=$dateType)" -} diff --git a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashBoardApiEnhancerFactory.kt b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashBoardApiEnhancerFactory.kt index af24ae3..9eea113 100644 --- a/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashBoardApiEnhancerFactory.kt +++ b/data-dashboard-backend/src/main/java/org/radarbase/datadashboard/api/enhancer/DashBoardApiEnhancerFactory.kt @@ -20,7 +20,6 @@ package org.radarbase.datadashboard.api.enhancer import org.radarbase.datadashboard.api.config.DashboardApiConfig import org.radarbase.datadashboard.api.domain.model.Observation -import org.radarbase.datadashboard.api.domain.model.Variable import org.radarbase.jersey.enhancer.EnhancerFactory import org.radarbase.jersey.enhancer.Enhancers import org.radarbase.jersey.enhancer.JerseyResourceEnhancer @@ -41,7 +40,6 @@ class DashBoardApiEnhancerFactory( config.database.copy( managedClasses = listOf( Observation::class.jvmName, - Variable::class.jvmName ), ), ), 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 42f8108..6e806b4 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 @@ -18,11 +18,11 @@ package org.radarbase.datadashboard.api.enhancer +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.service.ObservationService -import jakarta.inject.Singleton -import org.glassfish.jersey.internal.inject.AbstractBinder import org.radarbase.jersey.enhancer.JerseyResourceEnhancer import org.radarbase.jersey.filter.Filters @@ -51,6 +51,5 @@ class DashboardApiEnhancer( bind(ObservationRepository::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 6f18fd1..ff6d948 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,38 +19,49 @@ package org.radarbase.datadashboard.api.resource import jakarta.annotation.Resource -import jakarta.ws.rs.* +import jakarta.ws.rs.Consumes +import jakarta.ws.rs.GET +import jakarta.ws.rs.NotFoundException +import jakarta.ws.rs.Path +import jakarta.ws.rs.PathParam +import jakarta.ws.rs.Produces import jakarta.ws.rs.container.ContainerRequestContext import jakarta.ws.rs.core.Context -import jakarta.ws.rs.core.Response -import org.radarbase.datadashboard.api.api.VariableListDto -import org.radarbase.datadashboard.api.service.ObservationService 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.auth.filter.RadarSecurityContext +import org.slf4j.LoggerFactory -@Path("subject/{subjectId}/topic/{topicId}") +@Path("project/{projectId}/subject/{subjectId}/topic/{topicId}") @Resource @Produces("application/json") @Consumes("application/json") @Authenticated class ObservationResource( @Context private val observationService: ObservationService, - @Context private val request: ContainerRequestContext + @Context private val request: ContainerRequestContext, ) { @GET @Path("observations") @NeedsPermission(Permission.MEASUREMENT_READ) fun getObservations( + @PathParam("projectId") projectId: String, @PathParam("subjectId") subjectId: String, - @PathParam("topicId") topicId: String - ): List> { -// if (request.securityContext != null && request.securityContext is RadarSecurityContext) { -// val userName = (request.securityContext as RadarSecurityContext).userPrincipal -// if (!subjectId.equals(userName)) throw NotFoundException("Subjects can only access their own data.") - return observationService.getObservations(topicId, subjectId) -// } -// return emptyList() + @PathParam("topicId") topicId: String, + ): ObservationListDto { + if (request.securityContext != null && request.securityContext is RadarSecurityContext) { + val userName = (request.securityContext as RadarSecurityContext).userPrincipal + log.info("User $userName is accessing observations for $subjectId") + if (!subjectId.equals(userName)) throw NotFoundException("Subjects can only request their own observations.") + return observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) + } + return ObservationListDto(emptyList()) + } + + companion object { + private val log = LoggerFactory.getLogger(ObservationResource::class.java) } } 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 7fe9047..e2bc479 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 @@ -19,14 +19,17 @@ package org.radarbase.datadashboard.api.service import jakarta.ws.rs.core.Context -import org.radarbase.datadashboard.api.api.VariableListDto +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 + @Context private val observationRepository: ObservationRepository, ) { - fun getObservations(topicId: String, subjectId: String): List> { - return this.observationRepository.getObservations(topicId, subjectId) + 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/main/resources/db/changelog/changes/20220309-create-database.xml b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml index c879c52..dc9154b 100644 --- a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml +++ b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220309-create-database.xml @@ -21,53 +21,39 @@ xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.6.xsd"> + - - Create topic tables - - - - + + + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - + + + - - + + - - - - - - - - - + + + - + + + + + + + diff --git a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-insert-mock-data.xml b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-insert-mock-data.xml index 5ef108d..4a4c0f5 100644 --- a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-insert-mock-data.xml +++ b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-insert-mock-data.xml @@ -22,38 +22,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd"> - + - - - - + tableName="observation" + usePreparedStatements="true"> + + - - - - - - - - - - - - - - - - diff --git a/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv new file mode 100644 index 0000000..2d8302c --- /dev/null +++ b/data-dashboard-backend/src/main/resources/db/changelog/changes/20220321-observations.csv @@ -0,0 +1,9 @@ +project;subject;source;topic;category;variable;value_numeric;value_textual;date;end_date +project-1;sub-1;source-1;questionnaire_answer;baseline_questions;Perceived_Pain_Score;5;NULL;2021-02-20 00:00:00;NULL +project-1;sub-1;source-1;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.J.Adams;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;questionnaire_answer;baseline_questions;Perceived_Pain_Score;2;NULL;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;questionnaire_answer;followup_questions;Name_Of_Physician;NULL;Dr.G.Washington;2022-05-20 00:00:00;NULL +project-1;sub-1;source-1;phone_battery_level;NULL;batteryLevel;5;NULL;2021-02-20 00:00:00;NULL +project-1;sub-1;source-1;phone_battery_level;NULL;status;NULL;CHARGING;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;phone_battery_level;NULL;batteryLevel;10;NULL;2021-02-20 00:00:00;NULL +project-1;sub-2;source-1;phone_battery_level;NULL;status;NULL;FULL;2021-02-20 00:00:00;NULL 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 c493544..d1dd263 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 @@ -28,14 +28,13 @@ import org.glassfish.jersey.test.ServletDeploymentContext import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory 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.datadashboard.api.config.DashboardApiConfig +import org.radarbase.datadashboard.api.resource.ObservationResource import org.radarbase.jersey.auth.AuthValidator import org.radarbase.jersey.auth.disabled.DisabledAuthValidator import org.radarbase.jersey.config.ConfigLoader - class DashboardIntegrationTest : JerseyTest() { lateinit var disabledAuthValidator: DisabledAuthValidator @@ -49,6 +48,7 @@ class DashboardIntegrationTest : JerseyTest() { bind(disabledAuthValidator).to(AuthValidator::class.java).ranked(1) } }) + resourceConfig.register(ObservationResource::class.java) return resourceConfig } @@ -69,25 +69,19 @@ class DashboardIntegrationTest : JerseyTest() { } @Test - @Disabled("Manageportal authentication does not yet work (always code 200 returned)") fun testGetObservationsNoToken() { - val response = - target("subject/f09c03f1-617d-4c2b-b093-4936f75092fa/topic/android_phone_battery_level/observations").request() - .get() + val response = target("project/project-1/sub-1/topic/phone_battery_level/observations").request().get() Assertions.assertEquals(401, response.status) } @Test fun testGetObservationsWithToken() { - val response = - target("subject/f09c03f1-617d-4c2b-b093-4936f75092fa/topic/android_phone_battery_level/observations") - .request() - .header(HttpHeaders.AUTHORIZATION, "Bearer " + "... encoded token ...") - .get() - val a = response.readEntity(String::class.java) + val response = 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) } // TODO add more tests that include the token validation. - } 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 new file mode 100644 index 0000000..a3e007e --- /dev/null +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/resource/ObservationResourceTest.kt @@ -0,0 +1,115 @@ +/* + * + * * 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.resource + +import jakarta.ws.rs.core.Application +import org.glassfish.hk2.utilities.binding.AbstractBinder +import org.glassfish.jersey.server.ResourceConfig +import org.glassfish.jersey.test.JerseyTest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +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.mapper.toDto +import org.radarbase.datadashboard.api.domain.model.Observation +import org.radarbase.datadashboard.api.service.ObservationService +import java.time.ZonedDateTime + +class ObservationResourceTest : JerseyTest() { + + @Mock + 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" + + override fun configure(): Application { + // Initialize all defined Mockito mocks (with @Mock annotation). + MockitoAnnotations.openMocks(this) + // Configure the Jersey Application. + val resourceConfig = ResourceConfig(ObservationResource::class.java) + // Register the ObservationService mock for dependency injection (needed by ObservationResource). + resourceConfig.register(object : AbstractBinder() { + override fun configure() { + bind(observationService).to(ObservationService::class.java) + } + }) + return resourceConfig + } + + @BeforeEach + fun init() { + // Create some fake observations that are returned by the service. + val observations: List = listOf(createObservation(), createObservation(), createObservation(), createObservation()) + // Create Dto that should be returned by the ObservationService. + observationListDto = ObservationListDto( + observations.map { it.toDto() }, + ) + } + + @Test + fun testGetObservations() { + // 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)) + } + + @Test + fun testGetObservations_failNoSubjectId() { + val response = target("project/project-1/subject//topic/topic-1/observations").request().get() + 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) + } + + @Test + fun testGetObservations_failNoProjectId() { + val response = target("project//subject/sub-1/topic/topic-1/observations").request().get() + assertEquals(404, response.status) + } + + private fun createObservation(): Observation { + return Observation( + project = "project-1", + subject = subjectId, + source = "source-1", + topic = "topic-1", + category = "category-1", + variable = "variable-1", + date = ZonedDateTime.now(), + valueTextual = "value1", + valueNumeric = null, + endDate = null, + ) + } +} 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/ObservationServiceTest.kt new file mode 100644 index 0000000..8c86c53 --- /dev/null +++ b/data-dashboard-backend/src/test/java/org/radarbase/datadashboard/api/service/ObservationServiceTest.kt @@ -0,0 +1,87 @@ +/* + * + * * 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 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.mapper.toDto +import org.radarbase.datadashboard.api.domain.model.Observation +import java.time.ZonedDateTime + +class ObservationServiceTest { + + // Create a Mockito mock of the ObservationRepository. This is instantiated in the init block. + @Mock + private lateinit var observationRepository: ObservationRepository + + private var observationId: Long = 1 + private val projectId = "project-1" + private val subjectId = "sub-1" + private val topicId = "topic-1" + + private val observationService: ObservationService + + init { + // Initialize all Mockito mocks. + MockitoAnnotations.openMocks(this) + observationService = ObservationService(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() { + // 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()) + + // Mock the repository to return the fake observations. + `when`(observationRepository.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId)).thenReturn(observations) + + // Call the ObservationService (class under test) to get the observations. + val result = observationService.getObservations(projectId = projectId, subjectId = subjectId, topicId = topicId) + + // Check if the result is as expected (observations transformed to ObservationListDto). + val expectedDto = ObservationListDto( + observations.map { it.toDto() }, + ) + assertEquals(expectedDto, result) + } + + private fun createObservation(): Observation { + return Observation( + project = "project-1", + subject = subjectId, + source = "source-1", + topic = "topic-1", + category = "category-1", + variable = "variable-1", + date = ZonedDateTime.now(), + valueTextual = "value1", + valueNumeric = null, + endDate = null, + ) + } +}