diff --git a/.env b/.env index c12985b99..df125359d 100644 --- a/.env +++ b/.env @@ -4,7 +4,7 @@ STELLIO_SEARCH_DB_DATABASE=stellio_search STELLIO_SUBSCRIPTION_DB_DATABASE=stellio_subscription POSTGRES_DBNAME=${STELLIO_SEARCH_DB_DATABASE},${STELLIO_SUBSCRIPTION_DB_DATABASE} -STELLIO_DOCKER_TAG=2.0.0 +STELLIO_DOCKER_TAG=2.1.0 STELLIO_AUTHENTICATION_ENABLED=false diff --git a/.github/pr-labeler.yml b/.github/pr-labeler.yml index 35ccf7d3a..8fc848881 100644 --- a/.github/pr-labeler.yml +++ b/.github/pr-labeler.yml @@ -2,5 +2,3 @@ feature: ['feature/*', 'feat/*'] fix: fix/* chore: chore/* refactoring: refactor/* -fixed-branch: fixed-branch-name - diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 835737639..b22c88cd0 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -3,11 +3,16 @@ on: pull_request: types: [opened] +permissions: + contents: read + jobs: pr-labeler: + permissions: + contents: read # for TimonVS/pr-labeler-action to read config file + pull-requests: write # for TimonVS/pr-labeler-action to add labels in PR runs-on: ubuntu-latest steps: - - uses: TimonVS/pr-labeler-action@v3.1.0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - uses: TimonVS/pr-labeler-action@v4.1.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 98b80f382..e2d781835 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,18 @@ Please note that the environment and scripts are validated on Ubuntu and macOS. We also provide an experimental configuration to deploy Stellio in a k8s cluster (only tested in Minikube as of now). For more information, please look at [the README](kubernetes/README.md) +## Docker images tagging + +Starting from version 2.0.0, a new scheme is used for tagging of Docker images: +* Releases are tagged with the version number, e.g., `stellio/stellio-search-service:2.0.0` +* `latest` tag is no longer used for releases as it can be dangerous (for instance, triggering an unwanted major + upgrade) +* Developement versions (automatically produced when a commit is pushed on the `develop` branch) are tagged with a + tag containing the `-dev` suffix, e.g., `stellio/stellio-search-service:2.1.0-dev` +* On each commit on the `develop` branch, an image with the `latest-dev` tag is produced, e.g., `stellio/stellio-search-service:latest-dev` + +The version number is obtained during the build process by using the `version` information in the `build.gradle.kts` file. + ## Development ### Developing on a service @@ -186,4 +198,4 @@ It mainly makes use of the following libraries and frameworks (dependencies of d | WireMock | APL v2 | | Testcontainers | MIT | -© 2020 - 2022 EGM +© 2020 - 2023 EGM diff --git a/api-gateway/Dockerfile b/api-gateway/Dockerfile index 5e841310c..9489adb55 100644 --- a/api-gateway/Dockerfile +++ b/api-gateway/Dockerfile @@ -1,7 +1,15 @@ -FROM adoptopenjdk/openjdk11:alpine-jre -RUN addgroup -S stellio && adduser -S stellio -G stellio -USER stellio:stellio +# You can build a Docker image of the module with the following command: +# docker build --build-arg JAR_FILE=build/libs/api-gateway-{version}.jar -t stellio-api-gateway:{version} . +FROM eclipse-temurin:17-jre as builder +WORKDIR application ARG JAR_FILE=build/libs/*.jar -COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract +FROM eclipse-temurin:17-jre +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] diff --git a/api-gateway/build.gradle.kts b/api-gateway/build.gradle.kts index 4ee928cce..3eadbff09 100644 --- a/api-gateway/build.gradle.kts +++ b/api-gateway/build.gradle.kts @@ -7,15 +7,14 @@ plugins { dependencies { implementation("org.springframework.cloud:spring-cloud-starter-gateway") - implementation("org.springframework.boot:spring-boot-starter-oauth2-client") - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") } springBoot { buildInfo { properties { - name = "Stellio Context Broker" + name.set("Stellio Context Broker") } } } diff --git a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt index c9a141c30..bc7063f17 100644 --- a/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt +++ b/api-gateway/src/main/kotlin/com/egm/stellio/apigateway/ApiGatewayApplication.kt @@ -12,6 +12,7 @@ import reactor.netty.transport.logging.AdvancedByteBufFormat @SpringBootApplication class ApiGatewayApplication { + @Value("\${application.search-service.url:search-service}") private val searchServiceUrl: String = "" @@ -31,29 +32,15 @@ class ApiGatewayApplication { "/ngsi-ld/v1/entityOperations/**", "/ngsi-ld/v1/entityAccessControl/**", "/ngsi-ld/v1/types/**", - "/ngsi-ld/v1/attributes/**" - ) - .filters { - it.tokenRelay() - } - .uri("http://$searchServiceUrl:8083") - } - .route { p -> - p.path( + "/ngsi-ld/v1/attributes/**", "/ngsi-ld/v1/temporal/entities/**", "/ngsi-ld/v1/temporal/entityOperations/**" - ) - .filters { - it.tokenRelay() - } - .uri("http://$searchServiceUrl:8083") + ).uri("http://$searchServiceUrl:8083") } .route { p -> - p.path("/ngsi-ld/v1/subscriptions/**") - .filters { - it.tokenRelay() - } - .uri("http://$subscriptionServiceUrl:8084") + p.path( + "/ngsi-ld/v1/subscriptions/**" + ).uri("http://$subscriptionServiceUrl:8084") } .build() } diff --git a/api-gateway/src/main/resources/application-docker.yml b/api-gateway/src/main/resources/application-docker.yml new file mode 100644 index 000000000..7ab955e7b --- /dev/null +++ b/api-gateway/src/main/resources/application-docker.yml @@ -0,0 +1,5 @@ +application: + search-service: + url: search-service + subscription-service: + url: subscription-service diff --git a/api-gateway/src/main/resources/application.yml b/api-gateway/src/main/resources/application.yml index 46e049cb7..f0692e4f4 100644 --- a/api-gateway/src/main/resources/application.yml +++ b/api-gateway/src/main/resources/application.yml @@ -1,22 +1,4 @@ spring: - security: - oauth2: - client: - registration: - login-client: - provider: keycloak - client-id: api-gateway - client-secret: client-secret - authorization-grant-type: authorization_code - redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" - scope: openid,profile,email,resource.read - provider: - keycloak: - authorization-uri: http://127.0.0.1:8081/auth/realms/stellio/protocol/openid-connect/auth - token-uri: http://127.0.0.1:8081/auth/realms/stellio/protocol/openid-connect/token - user-info-uri: http://127.0.0.1:8081/auth/realms/stellio/protocol/openid-connect/userinfo - user-name-attribute: sub - jwk-set-uri: http://127.0.0.1:8081/auth/realms/stellio/protocol/openid-connect/certs cloud: gateway: routes: @@ -25,14 +7,12 @@ spring: predicates: - Path=/search-service/actuator/** filters: - - TokenRelay= - RewritePath=/search-service/actuator, /actuator - id: subscription_service_actuator uri: http://subscription-service:8084 predicates: - Path=/subscription-service/actuator/** filters: - - TokenRelay= - RewritePath=/subscription-service/actuator, /actuator globalcors: corsConfigurations: @@ -45,6 +25,12 @@ spring: - DELETE allowedHeaders: "*" +application: + search-service: + url: localhost + subscription-service: + url: localhost + management: endpoints: enabled-by-default: false diff --git a/build.gradle.kts b/build.gradle.kts index f93b73b78..74738618e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,20 +11,22 @@ buildscript { } } -extra["springCloudVersion"] = "2021.0.3" +extra["springCloudVersion"] = "2022.0.0" extra["testcontainersVersion"] = "1.17.5" plugins { - java // why did I have to add that ?! + // https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#reacting-to-other-plugins.java + java // only apply the plugin in the subprojects requiring it because it expects a Spring Boot app // and the shared lib is obviously not one - id("org.springframework.boot") version "2.7.5" apply false + id("org.springframework.boot") version "3.0.2" apply false id("io.spring.dependency-management") version "1.1.0" apply false - kotlin("jvm") version "1.6.21" apply false - kotlin("plugin.spring") version "1.6.21" apply false - id("org.jlleitschuh.gradle.ktlint") version "11.0.0" + id("org.graalvm.buildtools.native") version "0.9.19" + kotlin("jvm") version "1.8.10" apply false + kotlin("plugin.spring") version "1.8.10" apply false + id("org.jlleitschuh.gradle.ktlint") version "11.1.0" id("com.google.cloud.tools.jib") version "3.3.1" apply false - id("io.gitlab.arturbosch.detekt") version "1.21.0" apply false + id("io.gitlab.arturbosch.detekt") version "1.22.0" apply false id("org.sonarqube") version "3.5.0.2730" jacoco } @@ -68,7 +70,7 @@ subprojects { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.github.jsonld-java:jsonld-java:0.13.4") - implementation("io.arrow-kt:arrow-fx-coroutines:1.1.3") + implementation("io.arrow-kt:arrow-fx-coroutines:1.1.5") implementation("org.locationtech.jts.io:jts-io-common:1.19.0") @@ -78,11 +80,10 @@ subprojects { runtimeOnly("io.micrometer:micrometer-registry-prometheus") testImplementation("org.springframework.boot:spring-boot-starter-test") { - // to ensure we are using mocks and spies from springmockk lib instead + // to ensure we are using mocks and spies from springmockk (and not from Mockito) exclude(module = "mockito-core") } - testImplementation("com.ninja-squad:springmockk:3.1.2") - testImplementation("io.mockk:mockk:1.13.3") + testImplementation("com.ninja-squad:springmockk:4.0.0") testImplementation("io.projectreactor:reactor-test") testImplementation("org.springframework.security:spring-security-test") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") @@ -173,7 +174,7 @@ subprojects { allprojects { group = "com.egm.stellio" - version = "2.0.0" + version = "2.1.0" repositories { mavenCentral() diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 12cf18154..c4099c7c8 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -14,6 +14,7 @@ complexity: excludes: ['**/src/test/**'] LargeClass: excludes: ['**/src/test/**'] + threshold: 1000 NamedArguments: active: true ReplaceSafeCallChainWithRun: @@ -79,11 +80,18 @@ style: # DataClassShouldBeImmutable: # active: true DestructuringDeclarationWithTooManyEntries: - active: false + active: true ExplicitCollectionElementAccessMethod: active: true ExpressionBodySyntax: active: true + ForbiddenSuppress: + active: true + MultilineLambdaItParameter: + active: true + MultilineRawStringIndentation: + active: true + indentSize: 0 NoTabs: active: true OptionalWhenBraces: @@ -94,9 +102,11 @@ style: active: true TrailingWhitespace: active: true + UnnecessaryLet: + active: true UnnecessaryParentheses: active: true - UnnecessaryLet: + UnusedImports: active: true UseDataClass: active: true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d2880ba80..070cb702f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/search-service/Dockerfile b/search-service/Dockerfile index 5e841310c..0e4c15d6b 100644 --- a/search-service/Dockerfile +++ b/search-service/Dockerfile @@ -1,7 +1,15 @@ -FROM adoptopenjdk/openjdk11:alpine-jre -RUN addgroup -S stellio && adduser -S stellio -G stellio -USER stellio:stellio +# You can build a Docker image of the module with the following command: +# docker build --build-arg JAR_FILE=build/libs/search-service-{version}.jar -t stellio-search-service:{version} . +FROM eclipse-temurin:17-jre as builder +WORKDIR application ARG JAR_FILE=build/libs/*.jar -COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract +FROM eclipse-temurin:17-jre +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] diff --git a/search-service/build.gradle.kts b/search-service/build.gradle.kts index fb21fca4d..e38e1cb86 100644 --- a/search-service/build.gradle.kts +++ b/search-service/build.gradle.kts @@ -22,7 +22,7 @@ dependencies { implementation("com.savvasdalkitsis:json-merge:0.0.6") implementation(project(":shared")) - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") developmentOnly("org.springframework.boot:spring-boot-devtools") diff --git a/search-service/config/detekt/baseline.xml b/search-service/config/detekt/baseline.xml index c68fa512f..c4ecf20d3 100644 --- a/search-service/config/detekt/baseline.xml +++ b/search-service/config/detekt/baseline.xml @@ -6,7 +6,7 @@ ClassNaming:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration : BaseJavaMigration ComplexCondition:EntityHandler.kt$EntityHandler$queryParams.ids.isEmpty() && queryParams.q.isNullOrEmpty() && queryParams.types.isEmpty() && queryParams.attrs.isEmpty() ComplexCondition:EntityPayloadService.kt$EntityPayloadService$it && !inverse || !it && inverse - LargeClass:TemporalEntityAttributeService.kt$TemporalEntityAttributeService + Filename:db.migration.V0_29__JsonLd_migration.kt:1 LongMethod:AttributeInstanceService.kt$AttributeInstanceService$@Transactional suspend fun create(attributeInstance: AttributeInstance): Either<APIException, Unit> LongMethod:EntityAccessControlHandler.kt$EntityAccessControlHandler$@PostMapping("/{subjectId}/attrs", consumes = [MediaType.APPLICATION_JSON_VALUE, JSON_LD_CONTENT_TYPE]) suspend fun addRightsOnEntities( @RequestHeader httpHeaders: HttpHeaders, @PathVariable subjectId: String, @RequestBody requestBody: Mono<String> ): ResponseEntity<*> LongMethod:EntityEventServiceTests.kt$EntityEventServiceTests$@Test fun `it should publish ATTRIBUTE_APPEND and ATTRIBUTE_REPLACE events if attributes were appended and replaced`() @@ -25,7 +25,6 @@ LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( entityId: URI, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributePayloadEntry, sub: Sub? ) LongParameterList:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$( temporalEntityAttribute: TemporalEntityAttribute, ngsiLdAttribute: NgsiLdAttribute, attributeMetadata: AttributeMetadata, createdAt: ZonedDateTime, attributePayload: ExpandedAttributePayloadEntry, sub: Sub? ) LongParameterList:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$( entityId: URI, attributeName: ExpandedTerm, datasetId: URI?, attributePayload: ExpandedAttributePayloadEntry, ngsiLdAttributeInstance: NgsiLdAttributeInstance, defaultCreatedAt: ZonedDateTime ) - MaxLineLength:TemporalEntityAttributeService.kt$TemporalEntityAttributeService$ (@. NestedBlockDepth:V0_29__JsonLd_migration.kt$V0_29__JsonLd_migration$override fun migrate(context: Context) SwallowedException:QueryUtils.kt$e: IllegalArgumentException ThrowsCount:QueryUtils.kt$fun buildTemporalQuery(params: MultiValueMap<String, String>, inQueryEntities: Boolean = false): TemporalQuery diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt index 6ac151c6b..f3c866b39 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EnabledAuthorizationService.kt @@ -141,7 +141,6 @@ class EnabledAuthorizationService( limit: Int, sub: Option ): Either>> { - return either { val groups = when (userIsAdmin(sub)) { @@ -180,15 +179,15 @@ class EnabledAuthorizationService( } else { { """ - ( - (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') - OR - (tea.entity_id IN ( - SELECT entity_id - FROM entity_access_rights - WHERE subject_id IN (${it.toListOfString()}) - )) - ) + ( + (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') + OR + (tea.entity_id IN ( + SELECT entity_id + FROM entity_access_rights + WHERE subject_id IN (${it.toListOfString()}) + )) + ) """.trimIndent() } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt index 5971aa268..c9c342f26 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsService.kt @@ -80,7 +80,7 @@ class EntityAccessRightsService( .bind("entity_id", entityId) .bind("subject_id", sub) .executeExpected { - if (it == 0) + if (it == 0L) ResourceNotFoundException("No right found for $sub on $entityId").left() else Unit.right() } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/config/ApplicationProperties.kt b/search-service/src/main/kotlin/com/egm/stellio/search/config/ApplicationProperties.kt index 6b3abc52a..008c25c06 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/config/ApplicationProperties.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/config/ApplicationProperties.kt @@ -1,9 +1,7 @@ package com.egm.stellio.search.config import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.context.properties.ConstructorBinding -@ConstructorBinding @ConfigurationProperties("application") data class ApplicationProperties( val authentication: Authentication, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt b/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt index 1f0eca393..115c104c2 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/config/WebConfig.kt @@ -3,6 +3,7 @@ package com.egm.stellio.search.config import org.springframework.context.annotation.Configuration import org.springframework.http.codec.ServerCodecConfigurer import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.config.PathMatchConfigurer import org.springframework.web.reactive.config.WebFluxConfigurer @Configuration @@ -12,4 +13,8 @@ class WebConfig : WebFluxConfigurer { override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { configurer.defaultCodecs().enableLoggingRequestDetails(true) } + + override fun configurePathMatching(configurer: PathMatchConfigurer) { + configurer.setUseTrailingSlashMatch(true) + } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt b/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt index d56e21251..d3dd7c18e 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/listener/ObservationEventListener.kt @@ -58,7 +58,7 @@ class ObservationEventListener( observationEvent.operationPayload, observationEvent.contexts, observationEvent.sub - ).tap { + ).onRight { entityEventService.publishEntityCreateEvent( observationEvent.sub, observationEvent.entityId, diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt index 5e01edfa0..3d8dd7221 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/AttributeInstanceService.kt @@ -221,7 +221,7 @@ class AttributeInstanceService( ): ZonedDateTime? { var selectQuery = """ - SELECT min(time) as first + SELECT min(time) as first """.trimIndent() selectQuery = @@ -244,7 +244,7 @@ class AttributeInstanceService( return databaseClient .sql(selectQuery) .oneToResult { toZonedDateTime(it["first"]) } - .orNull() + .getOrNull() } private fun rowToAttributeInstanceResult( @@ -277,14 +277,14 @@ class AttributeInstanceService( ): Either { val deleteQuery = """ - DELETE FROM attribute_instance - WHERE temporal_entity_attribute = ( - SELECT id - FROM temporal_entity_attribute - WHERE entity_id = :entity_id - AND attribute_name = :attribute_name - ) - AND instance_id = :instance_id + DELETE FROM attribute_instance + WHERE temporal_entity_attribute = ( + SELECT id + FROM temporal_entity_attribute + WHERE entity_id = :entity_id + AND attribute_name = :attribute_name + ) + AND instance_id = :instance_id """.trimIndent() return databaseClient @@ -293,7 +293,7 @@ class AttributeInstanceService( .bind("attribute_name", attributeName) .bind("instance_id", instanceId) .executeExpected { - if (it == 0) + if (it == 0L) ResourceNotFoundException(instanceNotFoundMessage(instanceId.toString())).left() else Unit.right() } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt index d76b68ce4..122548a78 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityOperationService.kt @@ -10,7 +10,6 @@ import com.egm.stellio.search.util.prepareTemporalAttributes import com.egm.stellio.search.web.BatchEntityError import com.egm.stellio.search.web.BatchEntitySuccess import com.egm.stellio.search.web.BatchOperationResult -import com.egm.stellio.shared.model.APIException import com.egm.stellio.shared.model.BadRequestDataException import com.egm.stellio.shared.model.JsonLdEntity import com.egm.stellio.shared.model.NgsiLdEntity @@ -65,20 +64,23 @@ class EntityOperationService( ): BatchOperationResult { val creationResults = entities.map { val ngsiLdEntity = it - either { + either { val jsonLdEntity = jsonLdEntities.find { jsonLdEntity -> ngsiLdEntity.id.toString() == jsonLdEntity.id }!! ngsiLdEntity.prepareTemporalAttributes() .flatMap { attributesMetadata -> temporalEntityAttributeService.createEntityTemporalReferences( - ngsiLdEntity, jsonLdEntity, attributesMetadata, sub + ngsiLdEntity, + jsonLdEntity, + attributesMetadata, + sub ) - }.bimap({ apiException -> - BatchEntityError(ngsiLdEntity.id, arrayListOf(apiException.message)) - }, { + }.map { BatchEntitySuccess(ngsiLdEntity.id) - }).bind() + }.mapLeft { apiException -> + BatchEntityError(ngsiLdEntity.id, arrayListOf(apiException.message)) + }.bind() } }.fold( initial = Pair(listOf(), listOf()), @@ -90,21 +92,20 @@ class EntityOperationService( } ) - return BatchOperationResult( - creationResults.second.toMutableList(), creationResults.first.toMutableList() - ) + return BatchOperationResult(creationResults.second.toMutableList(), creationResults.first.toMutableList()) } suspend fun delete(entitiesIds: Set): BatchOperationResult { val deletionResults = entitiesIds.map { val entityId = it - either { + either { temporalEntityAttributeService.deleteTemporalEntityReferences(entityId) - .bimap({ apiException -> - BatchEntityError(entityId, arrayListOf(apiException.message)) - }, { + .map { BatchEntitySuccess(entityId) - }).bind() + } + .mapLeft { apiException -> + BatchEntityError(entityId, arrayListOf(apiException.message)) + }.bind() } }.fold( initial = Pair(listOf(), listOf()), @@ -187,7 +188,7 @@ class EntityOperationService( disallowOverwrite: Boolean, sub: Sub? ): Either = - either { + either { val (ngsiLdEntity, jsonLdEntity) = entity temporalEntityAttributeService.deleteTemporalAttributesOfEntity(ngsiLdEntity.id).bind() val updateResult = entityPayloadService.updateTypes( @@ -208,11 +209,11 @@ class EntityOperationService( updateResult.notUpdated.joinToString(", ") { it.attributeName + " : " + it.reason } ).left().bind() else updateResult.right().bind() - }.bimap({ - BatchEntityError(entity.first.id, arrayListOf(it.message)) - }, { + }.map { BatchEntitySuccess(entity.first.id) - }) + }.mapLeft { + BatchEntityError(entity.first.id, arrayListOf(it.message)) + } /* * Transactional because it should not replace entity attributes if new ones could not be replaced. @@ -223,7 +224,7 @@ class EntityOperationService( disallowOverwrite: Boolean, sub: Sub? ): Either = - either { + either { val (ngsiLdEntity, jsonLdEntity) = entity val updateResult = entityPayloadService.updateTypes( ngsiLdEntity.id, @@ -244,9 +245,9 @@ class EntityOperationService( BadRequestDataException( ArrayList(updateResult.notUpdated.map { it.attributeName + " : " + it.reason }).joinToString() ).left().bind() - }.bimap({ - BatchEntityError(entity.first.id, arrayListOf(it.message)) - }, { + }.map { BatchEntitySuccess(entity.first.id, it) - }) + }.mapLeft { + BatchEntityError(entity.first.id, arrayListOf(it.message)) + } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt index 74762b25f..ac600a6e6 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/EntityPayloadService.kt @@ -108,12 +108,12 @@ class EntityPayloadService( ): Either { val selectQuery = """ - select - exists( - select 1 - from entity_payload - where entity_id = :entity_id - ) as entityExists; + select + exists( + select 1 + from entity_payload + where entity_id = :entity_id + ) as entityExists; """.trimIndent() return databaseClient @@ -155,11 +155,12 @@ class EntityPayloadService( return emptyList() } - val query = """ + val query = + """ select entity_id from entity_payload where entity_id in (:entities_ids) - """.trimIndent() + """.trimIndent() return databaseClient .sql(query) diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt index e0f883edc..ecb99fe5b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/QueryService.kt @@ -38,14 +38,15 @@ class QueryService( queryParams, accessRightFilter ) - if (entitiesIds.isEmpty()) - return@either Pair, Int>(emptyList(), 0) - val count = temporalEntityAttributeService.getCountForEntities( queryParams, accessRightFilter ).bind() + // we can have an empty list of entities with a non-zero count (e.g., offset too high) + if (entitiesIds.isEmpty()) + return@either Pair, Int>(emptyList(), count) + val entitiesPayloads = entityPayloadService.retrieve(entitiesIds) .map { toJsonLdEntity(it, listOf(queryParams.context)) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerService.kt index bcbf223b3..d610c3c8d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/SubscriptionEventListenerService.kt @@ -161,9 +161,7 @@ class SubscriptionEventListenerService( payload = payload ) attributeInstanceService.create(attributeInstance).bind() - temporalEntityAttributeService.updateStatus( - tea.id, ZonedDateTime.now(ZoneOffset.UTC), payload - ).bind() + temporalEntityAttributeService.updateStatus(tea.id, ZonedDateTime.now(ZoneOffset.UTC), payload).bind() } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt b/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt index 683ae375c..ac52cae89 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeService.kt @@ -19,6 +19,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdEntity import com.egm.stellio.shared.util.JsonLdUtils.expandJsonLdTerm import com.egm.stellio.shared.util.JsonLdUtils.getAttributeFromExpandedAttributes import com.egm.stellio.shared.util.JsonLdUtils.getPropertyValueFromMap +import com.egm.stellio.shared.util.JsonLdUtils.getPropertyValueFromMapAsDateTime import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.savvasdalkitsis.jsonmerger.JsonMerger import io.r2dbc.postgresql.codec.Json @@ -402,21 +403,21 @@ class TemporalEntityAttributeService( val selectQuery = """ - WITH entities AS ( - SELECT DISTINCT(tea.entity_id) - FROM temporal_entity_attribute tea - JOIN entity_payload ON tea.entity_id = entity_payload.entity_id - WHERE $filterQuery - ORDER BY entity_id - LIMIT :limit - OFFSET :offset - ) - SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at, - dataset_id, payload - FROM temporal_entity_attribute - WHERE entity_id IN (SELECT entity_id FROM entities) - $filterOnAttributesQuery + WITH entities AS ( + SELECT DISTINCT(tea.entity_id) + FROM temporal_entity_attribute tea + JOIN entity_payload ON tea.entity_id = entity_payload.entity_id + WHERE $filterQuery ORDER BY entity_id + LIMIT :limit + OFFSET :offset + ) + SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at, + dataset_id, payload + FROM temporal_entity_attribute + WHERE entity_id IN (SELECT entity_id FROM entities) + $filterOnAttributesQuery + ORDER BY entity_id """.trimIndent() return databaseClient @@ -578,10 +579,10 @@ class TemporalEntityAttributeService( suspend fun getForEntity(id: URI, attrs: Set): List { val selectQuery = """ - SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at, - dataset_id, payload - FROM temporal_entity_attribute - WHERE entity_id = :entity_id + SELECT id, entity_id, attribute_name, attribute_type, attribute_value_type, created_at, modified_at, + dataset_id, payload + FROM temporal_entity_attribute + WHERE entity_id = :entity_id """.trimIndent() val expandedAttrsList = attrs.joinToString(",") { "'$it'" } @@ -673,19 +674,19 @@ class TemporalEntityAttributeService( ): Either { val selectQuery = """ - select - exists( - select 1 - from temporal_entity_attribute - where entity_id = :entity_id - ) as entityExists, - exists( - select 1 - from temporal_entity_attribute - where entity_id = :entity_id - and attribute_name = :attribute_name - ${datasetId.toDatasetIdFilter()} - ) as attributeNameExists; + select + exists( + select 1 + from temporal_entity_attribute + where entity_id = :entity_id + ) as entityExists, + exists( + select 1 + from temporal_entity_attribute + where entity_id = :entity_id + and attribute_name = :attribute_name + ${datasetId.toDatasetIdFilter()} + ) as attributeNameExists; """.trimIndent() return databaseClient @@ -768,7 +769,7 @@ class TemporalEntityAttributeService( null ).right() } - }.tap { + }.onRight { // update modifiedAt in entity if at least one attribute has been added if (it.hasSuccessfulUpdate()) { val teas = getForEntity(entityUri, emptySet()) @@ -830,7 +831,7 @@ class TemporalEntityAttributeService( message ).right() } - }.tap { + }.onRight { // update modifiedAt in entity if at least one attribute has been added if (it.hasSuccessfulUpdate()) { val teas = getForEntity(entityUri, emptySet()) @@ -876,9 +877,7 @@ class TemporalEntityAttributeService( val timeAndProperty = if (isNewObservation) Pair( - getPropertyValueFromMap( - attributeValues, NGSILD_OBSERVED_AT_PROPERTY - )!! as ZonedDateTime, + getPropertyValueFromMapAsDateTime(attributeValues, NGSILD_OBSERVED_AT_PROPERTY)!!, AttributeInstance.TemporalProperty.OBSERVED_AT ) else diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt b/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt index 075569e61..5ad105772 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/support/ApiTestsBootstrapper.kt @@ -100,8 +100,9 @@ class ApiTestsBootstrapper( SubjectReferential( subjectId = subjectId, subjectType = SubjectType.USER, - subjectInfo = """ - {"type":"Property","value":{"username":"$username"}} + subjectInfo = + """ + {"type":"Property","value":{"username":"$username"}} """.trimIndent(), globalRoles = globalRoles, groupsMemberships = @@ -114,14 +115,15 @@ class ApiTestsBootstrapper( SubjectReferential( subjectId = subjectId, subjectType = SubjectType.GROUP, - subjectInfo = """ - {"type":"Property","value":{"name":"$groupName"}} + subjectInfo = + """ + {"type":"Property","value":{"name":"$groupName"}} """.trimIndent() ) suspend fun createSubject(subjectId: String, subjectReferential: SubjectReferential) = subjectReferentialService.retrieve(subjectId) - .tapLeft { + .onLeft { subjectReferentialService.create(subjectReferential) } } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt index c1ab4fcd0..9701a4a6d 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/util/DBQueryUtils.kt @@ -45,7 +45,7 @@ suspend fun DatabaseClient.GenericExecuteSpec.execute(): Either Either + f: (value: Long) -> Either ): Either = this.fetch() .rowsUpdated() diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/util/QueryUtils.kt b/search-service/src/main/kotlin/com/egm/stellio/search/util/QueryUtils.kt index 86eaeae30..084649a7b 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/util/QueryUtils.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/util/QueryUtils.kt @@ -29,10 +29,12 @@ fun parseAndCheckQueryParams( throw BadRequestDataException("Either type or attrs need to be present in request parameters") val withTemporalValues = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), OptionsParamValue.TEMPORAL_VALUES + Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + OptionsParamValue.TEMPORAL_VALUES ) val withAudit = hasValueInOptionsParam( - Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), OptionsParamValue.AUDIT + Optional.ofNullable(requestParams.getFirst(QUERY_PARAM_OPTIONS)), + OptionsParamValue.AUDIT ) val temporalQuery = buildTemporalQuery(requestParams, inQueryEntities) @@ -59,11 +61,11 @@ fun buildTemporalQuery(params: MultiValueMap, inQueryEntities: B throw BadRequestDataException("'endTimeAt' request parameter is mandatory if 'timerel' is 'between'") val endTimeAt = endTimeAtParam?.parseTimeParameter("'endTimeAt' parameter is not a valid date") - ?.getOrHandle { + ?.getOrElse { throw BadRequestDataException(it) } - val (timerel, timeAt) = buildTimerelAndTime(timerelParam, timeAtParam, inQueryEntities).getOrHandle { + val (timerel, timeAt) = buildTimerelAndTime(timerelParam, timeAtParam, inQueryEntities).getOrElse { throw BadRequestDataException(it) } diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt index 7ae540b0c..df90f2f0c 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/AttributeHandler.kt @@ -27,7 +27,6 @@ class AttributeHandler( val mediaType = getApplicableMediaType(httpHeaders) val detailedRepresentation = details.orElse(false) return either> { - val availableAttribute: Any = if (detailedRepresentation) attributeService.getAttributeDetails(listOf(contextLink)) else diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt index 1a7342e9f..5c21d4963 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/EntityHandler.kt @@ -60,7 +60,10 @@ class EntityHandler( authorizationService.userCanCreateEntities(sub).bind() entityPayloadService.checkEntityExistence(ngsiLdEntity.id, true).bind() temporalEntityAttributeService.createEntityTemporalReferences( - ngsiLdEntity, jsonLdEntity, attributesMetadata, sub.orNull() + ngsiLdEntity, + jsonLdEntity, + attributesMetadata, + sub.orNull() ).bind() authorizationService.createAdminLink(ngsiLdEntity.id, sub).bind() @@ -436,17 +439,27 @@ class EntityHandler( val expandedAttrId = JsonLdUtils.expandJsonLdTerm(attrId, contexts) temporalEntityAttributeService.checkEntityAndAttributeExistence( - entityUri, expandedAttrId, datasetId + entityUri, + expandedAttrId, + datasetId ).bind() authorizationService.userCanUpdateEntity(entityUri, sub).bind() temporalEntityAttributeService.deleteTemporalAttribute( - entityUri, expandedAttrId, datasetId, deleteAll + entityUri, + expandedAttrId, + datasetId, + deleteAll ).bind() entityEventService.publishAttributeDeleteEvent( - sub.orNull(), entityUri, attrId, datasetId, deleteAll, contexts + sub.orNull(), + entityUri, + attrId, + datasetId, + deleteAll, + contexts ) ResponseEntity.status(HttpStatus.NO_CONTENT).build() }.fold( diff --git a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt index e27b2b075..3a4a01f78 100644 --- a/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt +++ b/search-service/src/main/kotlin/com/egm/stellio/search/web/TemporalEntityOperationsHandler.kt @@ -34,7 +34,6 @@ class TemporalEntityOperationsHandler( @RequestBody requestBody: Mono ): ResponseEntity<*> { return either> { - val sub = getSubFromSecurityContext() val contextLink = getContextFromLinkHeaderOrDefault(httpHeaders) val mediaType = getApplicableMediaType(httpHeaders) diff --git a/search-service/src/main/resources/application-docker.properties b/search-service/src/main/resources/application-docker.properties index 2f2761c71..5d9bcf205 100644 --- a/search-service/src/main/resources/application-docker.properties +++ b/search-service/src/main/resources/application-docker.properties @@ -3,6 +3,8 @@ spring.flyway.url = jdbc:postgresql://postgres/stellio_search spring.kafka.bootstrap-servers = kafka:9092 +server.error.include-stacktrace = never + spring.devtools.add-properties = false application.authentication.enabled = true diff --git a/search-service/src/main/resources/application.properties b/search-service/src/main/resources/application.properties index d90345a84..8bdf3cc99 100644 --- a/search-service/src/main/resources/application.properties +++ b/search-service/src/main/resources/application.properties @@ -15,8 +15,11 @@ spring.kafka.consumer.auto-offset-reset = earliest # By default, new matching topics are checked every 5 minutes but it can be configured by overriding the following prop # spring.kafka.consumer.properties.metadata.max.age.ms = 1000 -# cf https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#specifying-the-authorization-server +server.error.include-stacktrace = always + +# cf https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html#_specifying_the_authorization_server spring.security.oauth2.resourceserver.jwt.issuer-uri = https://sso.eglobalmark.com/auth/realms/stellio +# not required, but it avoids tying startup to authorization server's availability spring.security.oauth2.resourceserver.jwt.jwk-set-uri = https://sso.eglobalmark.com/auth/realms/stellio/protocol/openid-connect/certs application.authentication.enabled = false diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt index 22311d371..ad6b657ee 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/authorization/EntityAccessRightsServiceTests.kt @@ -249,7 +249,6 @@ class EntityAccessRightsServiceTests : WithTimescaleContainer { @Test fun `it should get all entities an user has access to`() = runTest { - createEntityPayload(entityId01, setOf(BEEHIVE_TYPE), AUTH_READ) createEntityPayload(entityId02, setOf(BEEHIVE_TYPE)) entityAccessRightsService.setRoleOnEntity(subjectUuid, entityId01, AccessRight.R_CAN_WRITE).shouldSucceed() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt index a4e8c847e..1992979cb 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/listener/ObservationEventListenerTests.kt @@ -212,11 +212,12 @@ class ObservationEventListenerTests { @Test fun `it should catch and drop any non compliant NGSI-LD payload`() = runTest { - val invalidObservationEvent = """ + val invalidObservationEvent = + """ { "id": "urn:ngsi-ld:Entity:01" } - """.trimIndent() + """.trimIndent() assertDoesNotThrow { observationEventListener.dispatchObservationMessage(invalidObservationEvent) @@ -228,11 +229,12 @@ class ObservationEventListenerTests { @Test fun `it should catch and drop any non compliant JSON-LD payload`() = runTest { - val invalidObservationEvent = """ + val invalidObservationEvent = + """ { "id": "urn:ngsi-ld:Entity:01",, } - """.trimIndent() + """.trimIndent() assertDoesNotThrow { observationEventListener.dispatchObservationMessage(invalidObservationEvent) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt index 5afa56fc8..64558a9a8 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityEventServiceTests.kt @@ -21,7 +21,7 @@ import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest import org.springframework.kafka.core.KafkaTemplate import org.springframework.test.context.ActiveProfiles -import org.springframework.util.concurrent.SettableListenableFuture +import java.util.concurrent.CompletableFuture @OptIn(ExperimentalCoroutinesApi::class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [EntityEventService::class]) @@ -69,7 +69,7 @@ class EntityEventServiceTests { @Test fun `it should cross publish all events on the catch-all topic`() { - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityEvent( EntityCreateEvent( @@ -89,7 +89,7 @@ class EntityEventServiceTests { @Test fun `it should only publish events for valid topic names`() { - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityEvent( EntityCreateEvent( @@ -111,10 +111,13 @@ class EntityEventServiceTests { coEvery { entityEventService.getSerializedEntity(any(), any()) } returns Pair(listOf(breedingServiceType), EMPTY_PAYLOAD).right() - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityCreateEvent( - null, breedingServiceUri, listOf(breedingServiceType), listOf(AQUAC_COMPOUND_CONTEXT) + null, + breedingServiceUri, + listOf(breedingServiceType), + listOf(AQUAC_COMPOUND_CONTEXT) ).join() verify { kafkaTemplate.send("cim.entity.BreedingService", breedingServiceUri.toString(), any()) } @@ -125,10 +128,13 @@ class EntityEventServiceTests { coEvery { entityEventService.getSerializedEntity(any(), any()) } returns Pair(listOf(breedingServiceType, feedingServiceType), EMPTY_PAYLOAD).right() - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityCreateEvent( - null, breedingServiceUri, listOf(breedingServiceType, feedingServiceType), listOf(AQUAC_COMPOUND_CONTEXT) + null, + breedingServiceUri, + listOf(breedingServiceType, feedingServiceType), + listOf(AQUAC_COMPOUND_CONTEXT) ).join() verify { @@ -142,10 +148,13 @@ class EntityEventServiceTests { coEvery { entityEventService.getSerializedEntity(any(), any()) } returns Pair(listOf(breedingServiceType), EMPTY_PAYLOAD).right() - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityReplaceEvent( - null, breedingServiceUri, listOf(breedingServiceType), listOf(AQUAC_COMPOUND_CONTEXT) + null, + breedingServiceUri, + listOf(breedingServiceType), + listOf(AQUAC_COMPOUND_CONTEXT) ).join() verify { kafkaTemplate.send("cim.entity.BreedingService", breedingServiceUri.toString(), any()) } @@ -153,10 +162,13 @@ class EntityEventServiceTests { @Test fun `it should publish an ENTITY_DELETE event`() = runTest { - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityDeleteEvent( - null, breedingServiceUri, listOf(breedingServiceType), listOf(AQUAC_COMPOUND_CONTEXT) + null, + breedingServiceUri, + listOf(breedingServiceType), + listOf(AQUAC_COMPOUND_CONTEXT) ).join() verify { kafkaTemplate.send("cim.entity.BreedingService", breedingServiceUri.toString(), any()) } @@ -164,10 +176,13 @@ class EntityEventServiceTests { @Test fun `it should publish two ENTITY_DELETE events if entity has two types`() = runTest { - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() entityEventService.publishEntityDeleteEvent( - null, breedingServiceUri, listOf(breedingServiceType, feedingServiceType), listOf(AQUAC_COMPOUND_CONTEXT) + null, + breedingServiceUri, + listOf(breedingServiceType, feedingServiceType), + listOf(AQUAC_COMPOUND_CONTEXT) ).join() verify { @@ -278,11 +293,11 @@ class EntityEventServiceTests { val jsonLdEntity = mockk(relaxed = true) val expectedOperationPayload = """ - { "type": "Property", "value": 120 } + { "type": "Property", "value": 120 } """.trimIndent() val fishNumberAttributeFragment = """ - { "fishNumber": $expectedOperationPayload } + { "fishNumber": $expectedOperationPayload } """.trimIndent() val jsonLdAttributes = expandJsonLdFragment(fishNumberAttributeFragment, listOf(AQUAC_COMPOUND_CONTEXT)) val appendResult = UpdateResult( @@ -328,19 +343,19 @@ class EntityEventServiceTests { val jsonLdEntity = mockk(relaxed = true) val expectedFishNumberOperationPayload = """ - { "type": "Property", "value": 120 } + { "type": "Property", "value": 120 } """.trimIndent() val fishNumberAttributeFragment = """ - "fishNumber": $expectedFishNumberOperationPayload + "fishNumber": $expectedFishNumberOperationPayload """.trimIndent() val expectedFishNameOperationPayload = """ - { "type": "Property", "datasetId": "$fishName1DatasetUri", "value": 50 } + { "type": "Property", "datasetId": "$fishName1DatasetUri", "value": 50 } """.trimIndent() val fishNameAttributeFragment = """ - "fishName": $expectedFishNameOperationPayload + "fishName": $expectedFishNameOperationPayload """.trimIndent() val attributesFragment = "{ $fishNumberAttributeFragment, $fishNameAttributeFragment }" val jsonLdAttributes = expandJsonLdFragment(attributesFragment, listOf(AQUAC_COMPOUND_CONTEXT)) @@ -405,19 +420,19 @@ class EntityEventServiceTests { val jsonLdEntity = mockk(relaxed = true) val expectedFishNumberOperationPayload = """ - { "type":"Property", "value":600 } + { "type":"Property", "value":600 } """.trimIndent() val fishNumberPayload = """ - "fishNumber": $expectedFishNumberOperationPayload + "fishNumber": $expectedFishNumberOperationPayload """.trimIndent() val expectedFishNameOperationPayload = """ - { "type":"Property", "datasetId": "$fishName1DatasetUri", "value":"Salmon", "unitCode": "C1" } + { "type":"Property", "datasetId": "$fishName1DatasetUri", "value":"Salmon", "unitCode": "C1" } """.trimIndent() val fishNamePayload = """ - "fishName": $expectedFishNameOperationPayload + "fishName": $expectedFishNameOperationPayload """.trimIndent() val attributePayload = "{ $fishNumberPayload, $fishNamePayload }" val jsonLdAttributes = expandJsonLdFragment(attributePayload, listOf(AQUAC_COMPOUND_CONTEXT)) diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt index 1f70a77f3..70eb037a8 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/EntityPayloadServiceTests.kt @@ -60,7 +60,11 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should create an entity payload from string if none existed yet`() = runTest { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ).shouldSucceed() } @@ -131,12 +135,20 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should not create an entity payload if one already existed`() = runTest { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ) assertThrows { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ) } } @@ -144,7 +156,11 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should retrieve an entity payload`() = runTest { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ).shouldSucceed() entityPayloadService.retrieve(entity01Uri) @@ -317,7 +333,11 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should upsert an entity payload if one already existed`() = runTest { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ) entityPayloadService.upsertEntityPayload(entity01Uri, EMPTY_PAYLOAD) @@ -365,7 +385,11 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { @Test fun `it should delete an entity payload`() = runTest { entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ) val deleteResult = entityPayloadService.deleteEntityPayload(entity01Uri) @@ -373,7 +397,11 @@ class EntityPayloadServiceTests : WithTimescaleContainer, WithKafkaContainer { // if correctly deleted, we should be able to create a new one entityPayloadService.createEntityPayload( - entity01Uri, listOf(BEEHIVE_TYPE), now, EMPTY_PAYLOAD, listOf(NGSILD_CORE_CONTEXT) + entity01Uri, + listOf(BEEHIVE_TYPE), + now, + EMPTY_PAYLOAD, + listOf(NGSILD_CORE_CONTEXT) ).shouldSucceed() } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt index 8deef6bc8..c15dfb780 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/IAMListenerTests.kt @@ -41,7 +41,7 @@ class IAMListenerTests { it.subjectType == SubjectType.USER && it.subjectInfo == """ - {"type":"Property","value":{"username":"stellio","givenName":"John","familyName":"Doe"}} + {"type":"Property","value":{"username":"stellio","givenName":"John","familyName":"Doe"}} """.trimIndent() && it.globalRoles == null } @@ -64,7 +64,7 @@ class IAMListenerTests { it.subjectType == SubjectType.CLIENT && it.subjectInfo == """ - {"type":"Property","value":{"clientId":"stellio-client"}} + {"type":"Property","value":{"clientId":"stellio-client"}} """.trimIndent() && it.globalRoles == null } @@ -87,7 +87,7 @@ class IAMListenerTests { it.subjectType == SubjectType.GROUP && it.subjectInfo == """ - {"type":"Property","value":{"name":"EGM"}} + {"type":"Property","value":{"name":"EGM"}} """.trimIndent() && it.globalRoles == null } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt index 9050349af..b352cccda 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/QueryServiceTests.kt @@ -97,14 +97,13 @@ class QueryServiceTests { @Test fun `it should return an empty list if no entity matched the query`() = runTest { coEvery { temporalEntityAttributeService.getForEntities(any(), any()) } returns emptyList() + coEvery { temporalEntityAttributeService.getCountForEntities(any(), any()) } returns 0.right() queryService.queryEntities(buildDefaultQueryParams()) { null } .shouldSucceedWith { assertEquals(0, it.second) assertTrue(it.first.isEmpty()) } - - coVerify { temporalEntityAttributeService.getCountForEntities(any(), any()) wasNot Called } } @Test diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt index 61ff7446f..858d77c1b 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityAttributeServiceTests.kt @@ -588,11 +588,11 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon ) ) { """ - ( - (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') - OR - (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')) - ) + ( + (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') + OR + (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')) + ) """.trimIndent() } @@ -630,11 +630,11 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon ) ) { """ - ( - (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') - OR - (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTE')) - ) + ( + (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') + OR + (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTE')) + ) """.trimIndent() } @@ -679,11 +679,11 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon ) ) { """ - ( - (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') - OR - (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')) - ) + ( + (specific_access_policy = 'AUTH_READ' OR specific_access_policy = 'AUTH_WRITE') + OR + (tea.entity_id IN ('urn:ngsi-ld:BeeHive:TESTD')) + ) """.trimIndent() } @@ -824,9 +824,8 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon temporalEntityAttributeService.deleteTemporalAttributesOfEntity(beehiveTestDId).shouldSucceed() - temporalEntityAttributeService.getForEntityAndAttribute( - beehiveTestDId, INCOMING_PROPERTY - ).shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } + temporalEntityAttributeService.getForEntityAndAttribute(beehiveTestDId, INCOMING_PROPERTY) + .shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } } @Test @@ -848,9 +847,8 @@ class TemporalEntityAttributeServiceTests : WithTimescaleContainer, WithKafkaCon attributeInstanceService.deleteInstancesOfAttribute(eq(beehiveTestDId), eq(INCOMING_PROPERTY), null) } - temporalEntityAttributeService.getForEntityAndAttribute( - beehiveTestDId, INCOMING_PROPERTY - ).shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } + temporalEntityAttributeService.getForEntityAndAttribute(beehiveTestDId, INCOMING_PROPERTY) + .shouldFail { assertInstanceOf(ResourceNotFoundException::class.java, it) } } @Test diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt index 1988aab2d..ee5141f3c 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/service/TemporalEntityServiceTests.kt @@ -139,8 +139,11 @@ class TemporalEntityServiceTests { ) ) val temporalQuery = TemporalQuery( - TemporalQuery.Timerel.AFTER, Instant.now().atZone(ZoneOffset.UTC).minusHours(1), - null, "1 day", TemporalQuery.Aggregate.SUM + TemporalQuery.Timerel.AFTER, + Instant.now().atZone(ZoneOffset.UTC).minusHours(1), + null, + "1 day", + TemporalQuery.Aggregate.SUM ) val entityPayload = EntityPayload( entityId = "urn:ngsi-ld:Subscription:1234".toUri(), diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt index bb0c9aa5c..2078ce6f7 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/AttributeHandlerTests.kt @@ -39,32 +39,32 @@ class AttributeHandlerTests { private val expectedAttributeDetails = """ - [ - { - "id":"https://ontology.eglobalmark.com/apic#temperature", - "type":"Attribute", - "attributeName":"temperature", - "typeNames":[ "BeeHive" ] - }, - { - "id":"https://ontology.eglobalmark.com/apic#incoming", - "type":"Attribute", - "attributeName":"incoming", - "typeNames":[ "BeeHive" ] - } - ] + [ + { + "id":"https://ontology.eglobalmark.com/apic#temperature", + "type":"Attribute", + "attributeName":"temperature", + "typeNames":[ "BeeHive" ] + }, + { + "id":"https://ontology.eglobalmark.com/apic#incoming", + "type":"Attribute", + "attributeName":"incoming", + "typeNames":[ "BeeHive" ] + } + ] """.trimIndent() private val expectedAttributeTypeInfo = """ - { - "id":"https://ontology.eglobalmark.com/apic#temperature", - "type":"Attribute", - "attributeName": "temperature", - "attributeTypes": ["Property"], - "typeNames": ["BeeHive"], - "attributeCount":2 - } + { + "id":"https://ontology.eglobalmark.com/apic#temperature", + "type":"Attribute", + "attributeName": "temperature", + "attributeTypes": ["Property"], + "typeNames": ["BeeHive"], + "attributeCount":2 + } """.trimIndent() @Test diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt index 8d06835b4..7abe6d4fb 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityAccessControlHandlerTests.kt @@ -350,11 +350,11 @@ class EntityAccessControlHandlerTests { .expectStatus().isNotFound .expectBody().json( """ - { - "detail": "No right found for urn:ngsi-ld:User:0123 on urn:ngsi-ld:Entity:entityId1", - "type": "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", - "title": "The referred resource has not been found" - } + { + "detail": "No right found for urn:ngsi-ld:User:0123 on urn:ngsi-ld:Entity:entityId1", + "type": "https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", + "title": "The referred resource has not been found" + } """.trimIndent() ) } @@ -453,11 +453,11 @@ class EntityAccessControlHandlerTests { .expectStatus().isBadRequest .expectBody().json( """ - { - "detail": "$expectedAttr is not authorized property name", - "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", - "title":"The request includes input data which does not meet the requirements of the operation" - } + { + "detail": "$expectedAttr is not authorized property name", + "type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData", + "title":"The request includes input data which does not meet the requirements of the operation" + } """.trimIndent() ) } @@ -602,28 +602,29 @@ class EntityAccessControlHandlerTests { .expectStatus().isOk .expectHeader().valueEquals(RESULTS_COUNT_HEADER, "2") .expectBody().json( - """[{ - "id": "urn:ngsi-ld:Beehive:TESTC", - "type": "$BEEHIVE_TYPE", - "$AUTH_TERM_RIGHT": {"type":"Property", "value": "rCanRead"}, - "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] + """ + [{ + "id": "urn:ngsi-ld:Beehive:TESTC", + "type": "$BEEHIVE_TYPE", + "$AUTH_TERM_RIGHT": {"type":"Property", "value": "rCanRead"}, + "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] + }, + { + "id": "urn:ngsi-ld:Beehive:TESTD", + "type": "$BEEHIVE_TYPE", + "$AUTH_TERM_RIGHT": {"type":"Property", "value": "rCanAdmin"}, + "$AUTH_TERM_SAP": {"type":"Property", "value": "$AUTH_READ"}, + "$AUTH_TERM_CAN_READ": { + "type":"Relationship", + "datasetId": "urn:ngsi-ld:Dataset:0123", + "object": "$subjectId", + "$AUTH_TERM_SUBJECT_INFO": { + "type":"Property", + "value": {"$AUTH_TERM_KIND": "User", "$AUTH_TERM_USERNAME": "stellio-user"} + } }, - { - "id": "urn:ngsi-ld:Beehive:TESTD", - "type": "$BEEHIVE_TYPE", - "$AUTH_TERM_RIGHT": {"type":"Property", "value": "rCanAdmin"}, - "$AUTH_TERM_SAP": {"type":"Property", "value": "$AUTH_READ"}, - "$AUTH_TERM_CAN_READ": { - "type":"Relationship", - "datasetId": "urn:ngsi-ld:Dataset:0123", - "object": "$subjectId", - "$AUTH_TERM_SUBJECT_INFO": { - "type":"Property", - "value": {"$AUTH_TERM_KIND": "User", "$AUTH_TERM_USERNAME": "stellio-user"} - } - }, - "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] - }] + "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] + }] """.trimMargin() ) .jsonPath("[0].createdAt").doesNotExist() @@ -696,14 +697,15 @@ class EntityAccessControlHandlerTests { .expectStatus().isOk .expectHeader().valueEquals(RESULTS_COUNT_HEADER, "1") .expectBody().json( - """[ - { - "id": "urn:ngsi-ld:group:1", - "type": "$GROUP_COMPACT_TYPE", - "name" : {"type":"Property", "value": "egm"}, - "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] - } - ] + """ + [ + { + "id": "urn:ngsi-ld:group:1", + "type": "$GROUP_COMPACT_TYPE", + "name" : {"type":"Property", "value": "egm"}, + "@context": ["${AuthContextModel.AUTHORIZATION_COMPOUND_CONTEXT}"] + } + ] """.trimMargin() ) .jsonPath("[0].createdAt").doesNotExist() @@ -736,15 +738,16 @@ class EntityAccessControlHandlerTests { .expectStatus().isOk .expectHeader().valueEquals(RESULTS_COUNT_HEADER, "1") .expectBody().json( - """[ - { - "id": "urn:ngsi-ld:group:01", - "type": "Group", - "name": {"type":"Property", "value": "egm"}, - "isMemberOf": {"type":"Property", "value": "true"}, - "@context": ["$AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] - } - ] + """ + [ + { + "id": "urn:ngsi-ld:group:01", + "type": "Group", + "name": {"type":"Property", "value": "egm"}, + "isMemberOf": {"type":"Property", "value": "true"}, + "@context": ["$AUTHORIZATION_CONTEXT", "$NGSILD_CORE_CONTEXT"] + } + ] """.trimMargin() ) .jsonPath("[0].createdAt").doesNotExist() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt index 14cbd4362..8c8e52a7f 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityHandlerTests.kt @@ -413,13 +413,13 @@ class EntityHandlerTests { .expectBody() .json( """ - { - "id": "$beehiveId", - "type": "Beehive", - "prop1": "some value", - "rel1": "urn:ngsi-ld:Entity:1234", - "@context": ["$NGSILD_CORE_CONTEXT"] - } + { + "id": "$beehiveId", + "type": "Beehive", + "prop1": "some value", + "rel1": "urn:ngsi-ld:Entity:1234", + "@context": ["$NGSILD_CORE_CONTEXT"] + } """.trimIndent() ) } @@ -437,7 +437,8 @@ class EntityHandlerTests { ).right() val expectedMessage = entityOrAttrsNotFoundMessage( - beehiveId.toString(), setOf("https://uri.etsi.org/ngsi-ld/default-context/attr2") + beehiveId.toString(), + setOf("https://uri.etsi.org/ngsi-ld/default-context/attr2") ) webClient.get() .uri("/ngsi-ld/v1/entities/$beehiveId?attrs=attr2") @@ -641,12 +642,12 @@ class EntityHandlerTests { .expectStatus().isOk .expectBody().json( """ - { - "id":"urn:ngsi-ld:Beehive:4567", - "type":"Beehive", - "name":{"type":"Property","datasetId":"urn:ngsi-ld:Property:french-name","value":"ruche"}, - "@context": ["$NGSILD_CORE_CONTEXT"] - } + { + "id":"urn:ngsi-ld:Beehive:4567", + "type":"Beehive", + "name":{"type":"Property","datasetId":"urn:ngsi-ld:Property:french-name","value":"ruche"}, + "@context": ["$NGSILD_CORE_CONTEXT"] + } """.trimIndent() ) } @@ -733,16 +734,16 @@ class EntityHandlerTests { .expectStatus().isOk .expectBody().json( """ - { - "id":"urn:ngsi-ld:Beehive:4567", - "type":"Beehive", - "managedBy": { - "type":"Relationship", - "datasetId":"urn:ngsi-ld:Dataset:managedBy:0215", - "object":"urn:ngsi-ld:Beekeeper:1230" - }, - "@context": ["$NGSILD_CORE_CONTEXT"] - } + { + "id":"urn:ngsi-ld:Beehive:4567", + "type":"Beehive", + "managedBy": { + "type":"Relationship", + "datasetId":"urn:ngsi-ld:Dataset:managedBy:0215", + "object":"urn:ngsi-ld:Beekeeper:1230" + }, + "@context": ["$NGSILD_CORE_CONTEXT"] + } """.trimIndent() ) } @@ -909,13 +910,14 @@ class EntityHandlerTests { .exchange() .expectStatus().isOk .expectBody().json( - """[ - { - "id": "$beehiveId", - "type": "Beehive", - "@context": ["$NGSILD_CORE_CONTEXT"] - } - ] + """ + [ + { + "id": "$beehiveId", + "type": "Beehive", + "@context": ["$NGSILD_CORE_CONTEXT"] + } + ] """.trimMargin() ) .jsonPath("[0].createdAt").doesNotExist() @@ -959,14 +961,15 @@ class EntityHandlerTests { .exchange() .expectStatus().isOk .expectBody().json( - """[ - { - "id": "$beehiveId", - "type": "Beehive", - "createdAt":"2015-10-18T11:20:30.000001Z", - "@context": ["$NGSILD_CORE_CONTEXT"] - } - ] + """ + [ + { + "id": "$beehiveId", + "type": "Beehive", + "createdAt":"2015-10-18T11:20:30.000001Z", + "@context": ["$NGSILD_CORE_CONTEXT"] + } + ] """.trimMargin() ) } @@ -998,13 +1001,14 @@ class EntityHandlerTests { "urn:ngsi-ld:Beehive:TESTD&limit=1&offset=2>;rel=\"next\";type=\"application/ld+json\"" ) .expectBody().json( - """[ - { - "id": "urn:ngsi-ld:Beehive:TESTC", - "type": "Beehive", - "@context": ["$NGSILD_CORE_CONTEXT"] - } - ] + """ + [ + { + "id": "urn:ngsi-ld:Beehive:TESTC", + "type": "Beehive", + "@context": ["$NGSILD_CORE_CONTEXT"] + } + ] """.trimMargin() ) } @@ -1087,13 +1091,14 @@ class EntityHandlerTests { .exchange() .expectStatus().isOk .expectBody().json( - """[ - { - "id": "$beehiveId", - "type": "Beehive", - "@context": ["$NGSILD_CORE_CONTEXT"] - } - ] + """ + [ + { + "id": "$beehiveId", + "type": "Beehive", + "@context": ["$NGSILD_CORE_CONTEXT"] + } + ] """.trimMargin() ) } diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt index 3b994f8fe..c0b2b6f30 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityOperationHandlerTests.kt @@ -241,15 +241,15 @@ class EntityOperationHandlerTests { .expectStatus().isEqualTo(HttpStatus.MULTI_STATUS) .expectBody().json( """ - { - "errors": [ - { - "entityId": "urn:ngsi-ld:Device:HCMR-AQUABOX1", - "error": [ "Entity does not exist" ] - } - ], - "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] - } + { + "errors": [ + { + "entityId": "urn:ngsi-ld:Device:HCMR-AQUABOX1", + "error": [ "Entity does not exist" ] + } + ], + "success": [ "urn:ngsi-ld:Sensor:HCMR-AQUABOX1temperature" ] + } """.trimIndent() ) } @@ -272,7 +272,10 @@ class EntityOperationHandlerTests { coEvery { authorizationService.createAdminLinks(any(), eq(sub)) } returns Unit.right() every { entityEventService.publishEntityCreateEvent( - any(), capture(capturedEntitiesIds), capture(capturedEntityTypes), any() + any(), + capture(capturedEntitiesIds), + capture(capturedEntityTypes), + any() ) } returns Job() @@ -318,7 +321,10 @@ class EntityOperationHandlerTests { coEvery { authorizationService.createAdminLinks(any(), eq(sub)) } returns Unit.right() every { entityEventService.publishEntityCreateEvent( - any(), capture(capturedEntitiesIds), capture(capturedEntityTypes), any() + any(), + capture(capturedEntitiesIds), + capture(capturedEntityTypes), + any() ) } returns Job() diff --git a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt index 15f7d172c..e0281eba9 100644 --- a/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt +++ b/search-service/src/test/kotlin/com/egm/stellio/search/web/EntityTypeHandlerTests.kt @@ -40,62 +40,62 @@ class EntityTypeHandlerTests { private val expectedEntityTypeInfo = """ - { - "id":"https://ontology.eglobalmark.com/apic#BeeHive", - "type":"EntityTypeInfo", - "typeName":"BeeHive", - "entityCount":2, - "attributeDetails":[ - { - "id":"https://ontology.eglobalmark.com/apic#temperature", - "type":"Attribute", - "attributeName":"temperature", - "attributeTypes":[ - "Property" - ] - }, - { - "id":"https://ontology.eglobalmark.com/egm#managedBy", - "type":"Attribute", - "attributeName":"managedBy", - "attributeTypes":[ - "Relationship" - ] - }, - { - "id":"https://uri.etsi.org/ngsi-ld/location", - "type":"Attribute", - "attributeName":"location", - "attributeTypes":[ - "GeoProperty" - ] - } - ] - } + { + "id":"https://ontology.eglobalmark.com/apic#BeeHive", + "type":"EntityTypeInfo", + "typeName":"BeeHive", + "entityCount":2, + "attributeDetails":[ + { + "id":"https://ontology.eglobalmark.com/apic#temperature", + "type":"Attribute", + "attributeName":"temperature", + "attributeTypes":[ + "Property" + ] + }, + { + "id":"https://ontology.eglobalmark.com/egm#managedBy", + "type":"Attribute", + "attributeName":"managedBy", + "attributeTypes":[ + "Relationship" + ] + }, + { + "id":"https://uri.etsi.org/ngsi-ld/location", + "type":"Attribute", + "attributeName":"location", + "attributeTypes":[ + "GeoProperty" + ] + } + ] + } """.trimIndent() private val expectedEntityTypes = """ - [ - { - "id":"https://ontology.eglobalmark.com/aquac#DeadFishes", - "type":"EntityType", - "typeName":"DeadFishes", - "attributeNames":[ - "https://ontology.eglobalmark.com/aquac#fishNumber", - "https://ontology.eglobalmark.com/aquac#removedFrom" - ] - }, - { - "id":"https://ontology.eglobalmark.com/egm#Sensor", - "type":"EntityType", - "typeName":"Sensor", - "attributeNames":[ - "https://ontology.eglobalmark.com/aquac#isContainedIn", - "https://ontology.eglobalmark.com/aquac#deviceParameter" - ] - } - ] + [ + { + "id":"https://ontology.eglobalmark.com/aquac#DeadFishes", + "type":"EntityType", + "typeName":"DeadFishes", + "attributeNames":[ + "https://ontology.eglobalmark.com/aquac#fishNumber", + "https://ontology.eglobalmark.com/aquac#removedFrom" + ] + }, + { + "id":"https://ontology.eglobalmark.com/egm#Sensor", + "type":"EntityType", + "typeName":"Sensor", + "attributeNames":[ + "https://ontology.eglobalmark.com/aquac#isContainedIn", + "https://ontology.eglobalmark.com/aquac#deviceParameter" + ] + } + ] """.trimIndent() @Test diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 8e169e7ad..e41b35148 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -22,13 +22,13 @@ dependencies { testFixturesImplementation("org.springframework.security:spring-security-oauth2-jose") testFixturesImplementation("org.springframework.security:spring-security-test") testFixturesImplementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") - testFixturesImplementation("io.arrow-kt:arrow-fx-coroutines:1.1.3") + testFixturesImplementation("io.arrow-kt:arrow-fx-coroutines:1.1.5") testFixturesImplementation("org.springframework.boot:spring-boot-starter-test") { // to ensure we are using mocks and spies from springmockk lib instead exclude(module = "mockito-core") } - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") testFixturesApi("org.testcontainers:testcontainers") testFixturesApi("org.testcontainers:junit-jupiter") diff --git a/shared/config/detekt/baseline.xml b/shared/config/detekt/baseline.xml index e602475fa..8c8adf1e0 100644 --- a/shared/config/detekt/baseline.xml +++ b/shared/config/detekt/baseline.xml @@ -2,6 +2,7 @@ + CyclomaticComplexMethod:ExceptionHandler.kt$ExceptionHandler$@ExceptionHandler fun transformErrorResponse(throwable: Throwable): ResponseEntity<ProblemDetail> LongParameterList:ApiResponses.kt$( body: String, count: Int, resourceUrl: String, queryParams: QueryParams, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contextLink: String ) LongParameterList:ApiResponses.kt$( entities: List<CompactedJsonLdEntity>, count: Int, resourceUrl: String, queryParams: QueryParams, requestParams: MultiValueMap<String, String>, mediaType: MediaType, contextLink: String ) LongParameterList:NgsiLdEntity.kt$NgsiLdGeoPropertyInstance$( val coordinates: WKTCoordinates, createdAt: ZonedDateTime?, modifiedAt: ZonedDateTime?, observedAt: ZonedDateTime? = null, datasetId: URI? = null, properties: List<NgsiLdProperty> = emptyList(), relationships: List<NgsiLdRelationship> = emptyList() ) @@ -10,6 +11,6 @@ NestedBlockDepth:JsonLdUtils.kt$JsonLdUtils$fun getPropertyValueFromMap(value: Map<String, List<Any>>, propertyKey: String): Any? SpreadOperator:EntityEvent.kt$EntityEvent$( *[ JsonSubTypes.Type(value = EntityCreateEvent::class), JsonSubTypes.Type(value = EntityReplaceEvent::class), JsonSubTypes.Type(value = EntityUpdateEvent::class), JsonSubTypes.Type(value = EntityDeleteEvent::class), JsonSubTypes.Type(value = AttributeAppendEvent::class), JsonSubTypes.Type(value = AttributeReplaceEvent::class), JsonSubTypes.Type(value = AttributeUpdateEvent::class), JsonSubTypes.Type(value = AttributeDeleteEvent::class), JsonSubTypes.Type(value = AttributeDeleteAllInstancesEvent::class) ] ) SwallowedException:JsonLdUtils.kt$JsonLdUtils$e: JsonLdError - TooManyFunctions:JsonLdUtils.kt$JsonLdUtils$JsonLdUtils + TooManyFunctions:JsonLdUtils.kt$JsonLdUtils diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt index 38b66fad9..b5ef1a0a8 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/ErrorResponse.kt @@ -1,7 +1,9 @@ package com.egm.stellio.shared.model +import java.net.URI + sealed class ErrorResponse( - val type: String, + val type: URI, open val title: String, open val detail: String ) @@ -67,26 +69,44 @@ data class JsonParseErrorResponse(override val detail: String) : ErrorResponse( data class AccessDeniedResponse(override val detail: String) : ErrorResponse( - "https://uri.etsi.org/ngsi-ld/errors/AccessDenied", + ErrorType.ACCESS_DENIED.type, "The request tried to access an unauthorized resource", detail ) data class NotImplementedResponse(override val detail: String) : ErrorResponse( - "https://uri.etsi.org/ngsi-ld/errors/NotImplemented", + ErrorType.NOT_IMPLEMENTED.type, "The requested functionality is not yet implemented", detail ) -enum class ErrorType(val type: String) { - INVALID_REQUEST("https://uri.etsi.org/ngsi-ld/errors/InvalidRequest"), - BAD_REQUEST_DATA("https://uri.etsi.org/ngsi-ld/errors/BadRequestData"), - ALREADY_EXISTS("https://uri.etsi.org/ngsi-ld/errors/AlreadyExists"), - OPERATION_NOT_SUPPORTED("https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported"), - RESOURCE_NOT_FOUND("https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound"), - INTERNAL_ERROR("https://uri.etsi.org/ngsi-ld/errors/InternalError"), - TOO_COMPLEX_QUERY("https://uri.etsi.org/ngsi-ld/errors/TooComplexQuery"), - TOO_MANY_RESULTS("https://uri.etsi.org/ngsi-ld/errors/TooManyResults"), - LD_CONTEXT_NOT_AVAILABLE("https://uri.etsi.org/ngsi-ld/errors/LdContextNotAvailable") +data class UnsupportedMediaTypeResponse(override val detail: String) : + ErrorResponse( + ErrorType.UNSUPPORTED_MEDIA_TYPE.type, + "The content type of the request is not supported", + detail + ) + +data class NotAcceptableResponse(override val detail: String) : + ErrorResponse( + ErrorType.NOT_ACCEPTABLE.type, + "The media type provided in Accept header is not supported", + detail + ) + +enum class ErrorType(val type: URI) { + INVALID_REQUEST(URI("https://uri.etsi.org/ngsi-ld/errors/InvalidRequest")), + BAD_REQUEST_DATA(URI("https://uri.etsi.org/ngsi-ld/errors/BadRequestData")), + ALREADY_EXISTS(URI("https://uri.etsi.org/ngsi-ld/errors/AlreadyExists")), + OPERATION_NOT_SUPPORTED(URI("https://uri.etsi.org/ngsi-ld/errors/OperationNotSupported")), + RESOURCE_NOT_FOUND(URI("https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound")), + INTERNAL_ERROR(URI("https://uri.etsi.org/ngsi-ld/errors/InternalError")), + TOO_COMPLEX_QUERY(URI("https://uri.etsi.org/ngsi-ld/errors/TooComplexQuery")), + TOO_MANY_RESULTS(URI("https://uri.etsi.org/ngsi-ld/errors/TooManyResults")), + LD_CONTEXT_NOT_AVAILABLE(URI("https://uri.etsi.org/ngsi-ld/errors/LdContextNotAvailable")), + ACCESS_DENIED(URI("https://uri.etsi.org/ngsi-ld/errors/AccessDenied")), + NOT_IMPLEMENTED(URI("https://uri.etsi.org/ngsi-ld/errors/NotImplemented")), + UNSUPPORTED_MEDIA_TYPE(URI("https://uri.etsi.org/ngsi-ld/errors/UnsupportedMediaType")), + NOT_ACCEPTABLE(URI("https://uri.etsi.org/ngsi-ld/errors/NotAcceptable")) } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt index 3569ac76c..cd99bf0d0 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/model/NgsiLdEntity.kt @@ -207,7 +207,14 @@ class NgsiLdPropertyInstance private constructor( throw BadRequestDataException("Property has unknown attributes types: $attributes") return NgsiLdPropertyInstance( - value, unitCode, createdAt, modifiedAt, observedAt, datasetId, properties, relationships + value, + unitCode, + createdAt, + modifiedAt, + observedAt, + datasetId, + properties, + relationships ) } } @@ -242,7 +249,13 @@ class NgsiLdRelationshipInstance private constructor( throw BadRequestDataException("Relationship has unknown attributes: $attributes") return NgsiLdRelationshipInstance( - objectId, createdAt, modifiedAt, observedAt, datasetId, properties, relationships + objectId, + createdAt, + modifiedAt, + observedAt, + datasetId, + properties, + relationships ) } } @@ -277,7 +290,13 @@ class NgsiLdGeoPropertyInstance( throw BadRequestDataException("Geoproperty has unknown attributes: $attributes") return NgsiLdGeoPropertyInstance( - WKTCoordinates(wktValue), createdAt, modifiedAt, observedAt, datasetId, properties, relationships + WKTCoordinates(wktValue), + createdAt, + modifiedAt, + observedAt, + datasetId, + properties, + relationships ) } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt index 82649b366..7328376af 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/ApiUtils.kt @@ -9,6 +9,7 @@ import com.egm.stellio.shared.util.JsonLdUtils.JSONLD_CONTEXT import com.egm.stellio.shared.util.JsonLdUtils.extractContextFromInput import org.springframework.http.HttpHeaders import org.springframework.http.MediaType +import org.springframework.util.MimeTypeUtils import org.springframework.util.MultiValueMap import org.springframework.web.server.NotAcceptableStatusException import java.time.ZonedDateTime @@ -153,7 +154,7 @@ fun getApplicableMediaType(httpHeaders: HttpHeaders): MediaType = fun List.getApplicable(): MediaType { if (this.isEmpty()) return MediaType.APPLICATION_JSON - MediaType.sortByQualityValue(this) + MimeTypeUtils.sortBySpecificity(this) val mediaType = this.find { it.includes(MediaType.APPLICATION_JSON) || it.includes(JSON_LD_MEDIA_TYPE) } ?: throw NotAcceptableStatusException(listOf(MediaType.APPLICATION_JSON, JSON_LD_MEDIA_TYPE)) @@ -169,7 +170,6 @@ fun parseAndCheckParams( requestParams: MultiValueMap, contextLink: String ): QueryParams { - val ids = requestParams.getFirst(QUERY_PARAM_ID)?.split(",").orEmpty().toListOfUri().toSet() val types = parseAndExpandRequestParameter(requestParams.getFirst(QUERY_PARAM_TYPE), contextLink) val idPattern = requestParams.getFirst(QUERY_PARAM_ID_PATTERN)?.also { idPattern -> diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt index ca0d198f9..0fff6a8d6 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/AuthUtils.kt @@ -12,7 +12,6 @@ import com.egm.stellio.shared.util.JsonLdUtils.NGSILD_CORE_CONTEXT import kotlinx.coroutines.reactive.awaitFirst import org.springframework.security.core.context.ReactiveSecurityContextHolder import org.springframework.security.core.context.SecurityContextImpl -import org.springframework.security.oauth2.jwt.Jwt import reactor.core.publisher.Mono import java.net.URI @@ -78,7 +77,8 @@ suspend fun getSubFromSecurityContext(): Option { return ReactiveSecurityContextHolder.getContext() .switchIfEmpty(Mono.just(SecurityContextImpl())) .map { context -> - context.authentication?.principal?.let { Some((it as Jwt).subject) } ?: None + // Authentication#getName maps to the JWT’s sub property, if one is present. + context.authentication?.name.toOption() } .awaitFirst() } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt index ff1081f63..f35e7ebca 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/util/JsonLdUtils.kt @@ -603,10 +603,10 @@ fun CompactedJsonLdEntity.toKeyValues(): Map = private fun simplifyRepresentation(value: Any): Any { return when (value) { // entity property value is always a Map - is Map<*, *> -> simplifyValue(value) + is Map<*, *> -> simplifyValue(value as Map) is List<*> -> value.map { when (it) { - is Map<*, *> -> simplifyValue(it) + is Map<*, *> -> simplifyValue(it as Map) // we keep @context value as it is (List) else -> it } @@ -616,10 +616,10 @@ private fun simplifyRepresentation(value: Any): Any { } } -private fun simplifyValue(value: Map<*, *>): Any { +private fun simplifyValue(value: Map): Any { return when (value["type"]) { - "Property", "GeoProperty" -> value.getOrDefault("value", value)!! - "Relationship" -> value.getOrDefault("object", value)!! + "Property", "GeoProperty" -> value.getOrDefault("value", value) + "Relationship" -> value.getOrDefault("object", value) else -> value } } diff --git a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt index a7f50ecbc..cee80e492 100644 --- a/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt +++ b/shared/src/main/kotlin/com/egm/stellio/shared/web/ExceptionHandler.kt @@ -1,16 +1,17 @@ package com.egm.stellio.shared.web import com.egm.stellio.shared.model.* -import com.egm.stellio.shared.util.JsonUtils.serializeObject import com.fasterxml.jackson.core.JsonParseException import com.github.jsonldjava.core.JsonLdError import org.slf4j.LoggerFactory import org.springframework.core.codec.CodecException import org.springframework.http.HttpStatus -import org.springframework.http.MediaType +import org.springframework.http.ProblemDetail import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.server.NotAcceptableStatusException +import org.springframework.web.server.UnsupportedMediaTypeStatusException @RestControllerAdvice class ExceptionHandler { @@ -18,7 +19,7 @@ class ExceptionHandler { private val logger = LoggerFactory.getLogger(javaClass) @ExceptionHandler - fun transformErrorResponse(throwable: Throwable): ResponseEntity = + fun transformErrorResponse(throwable: Throwable): ResponseEntity = when (val cause = throwable.cause ?: throwable) { is AlreadyExistsException -> generateErrorResponse( HttpStatus.CONFLICT, @@ -56,16 +57,28 @@ class ExceptionHandler { HttpStatus.SERVICE_UNAVAILABLE, LdContextNotAvailableResponse(cause.message) ) + is UnsupportedMediaTypeStatusException -> generateErrorResponse( + HttpStatus.UNSUPPORTED_MEDIA_TYPE, + UnsupportedMediaTypeResponse(cause.message) + ) + is NotAcceptableStatusException -> generateErrorResponse( + HttpStatus.NOT_ACCEPTABLE, + NotAcceptableResponse(cause.message) + ) else -> generateErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR, InternalErrorResponse("$cause") ) } - private fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity { + private fun generateErrorResponse(status: HttpStatus, exception: ErrorResponse): ResponseEntity { logger.info("Returning error ${exception.type} (${exception.detail})") return ResponseEntity.status(status) - .contentType(MediaType.APPLICATION_JSON) - .body(serializeObject(exception)) + .body( + ProblemDetail.forStatusAndDetail(status, exception.detail).also { + it.title = exception.title + it.type = exception.type + } + ) } } diff --git a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt index e216c5738..e5d2b9917 100644 --- a/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt +++ b/shared/src/test/kotlin/com/egm/stellio/shared/util/JsonUtilsTests.kt @@ -157,11 +157,11 @@ class JsonUtilsTests { } Assertions.assertEquals( """ - Unexpected character (',' (code 44)): was expecting double-quote to start field name - at [Source: (String)"{ - "id": "urn:ngsi-ld:Device:01234",, - "type": "Device" - }"; line: 2, column: 39] + Unexpected character (',' (code 44)): was expecting double-quote to start field name + at [Source: (String)"{ + "id": "urn:ngsi-ld:Device:01234",, + "type": "Device" + }"; line: 2, column: 39] """.trimIndent(), exception.message ) diff --git a/subscription-service/Dockerfile b/subscription-service/Dockerfile index 5e841310c..b0b229a42 100644 --- a/subscription-service/Dockerfile +++ b/subscription-service/Dockerfile @@ -1,7 +1,15 @@ -FROM adoptopenjdk/openjdk11:alpine-jre -RUN addgroup -S stellio && adduser -S stellio -G stellio -USER stellio:stellio +# You can build a Docker image of the module with the following command: +# docker build --build-arg JAR_FILE=build/libs/subscription-service-{version}.jar -t stellio-subscription-service:{version} . +FROM eclipse-temurin:17-jre as builder +WORKDIR application ARG JAR_FILE=build/libs/*.jar -COPY ${JAR_FILE} app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +COPY ${JAR_FILE} application.jar +RUN java -Djarmode=layertools -jar application.jar extract +FROM eclipse-temurin:17-jre +WORKDIR application +COPY --from=builder application/dependencies/ ./ +COPY --from=builder application/spring-boot-loader/ ./ +COPY --from=builder application/snapshot-dependencies/ ./ +COPY --from=builder application/application/ ./ +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] diff --git a/subscription-service/build.gradle.kts b/subscription-service/build.gradle.kts index 0451dea53..d6c70e041 100644 --- a/subscription-service/build.gradle.kts +++ b/subscription-service/build.gradle.kts @@ -23,13 +23,14 @@ dependencies { implementation("com.google.firebase:firebase-admin:9.1.1") implementation("org.springframework.boot:spring-boot-starter-oauth2-client") - detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.21.0") + detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.22.0") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("org.postgresql:postgresql") - testImplementation("com.github.tomakehurst:wiremock-standalone:2.27.2") + // see https://github.com/wiremock/wiremock/issues/1760 + testImplementation("com.github.tomakehurst:wiremock-jre8-standalone:2.35.0") testImplementation("org.testcontainers:postgresql") testImplementation("org.testcontainers:r2dbc") testImplementation(testFixtures(project(":shared"))) diff --git a/subscription-service/config/detekt/baseline.xml b/subscription-service/config/detekt/baseline.xml index 5579aa2d2..c3bc85a37 100644 --- a/subscription-service/config/detekt/baseline.xml +++ b/subscription-service/config/detekt/baseline.xml @@ -2,7 +2,6 @@ - LargeClass:SubscriptionService.kt$SubscriptionService LongParameterList:FixtureUtils.kt$( withQueryAndGeoQuery: Pair<Boolean, Boolean> = Pair(true, true), withEndpointInfo: Boolean = true, withNotifParams: Pair<FormatType, List<String>> = Pair(FormatType.NORMALIZED, emptyList()), withModifiedAt: Boolean = false, georel: String = "within", coordinates: Any = "[[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]]", timeInterval: Int? = null ) SwallowedException:QueryUtils.kt$QueryUtils$e: Exception SwallowedException:SubscriptionService.kt$SubscriptionService$e: Exception diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/ApplicationProperties.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/ApplicationProperties.kt index a6cce2e8c..1d853a713 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/ApplicationProperties.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/ApplicationProperties.kt @@ -1,9 +1,7 @@ package com.egm.stellio.subscription.config import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.boot.context.properties.ConstructorBinding -@ConstructorBinding @ConfigurationProperties("application") data class ApplicationProperties( val pagination: Pagination diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/WebConfig.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/WebConfig.kt new file mode 100644 index 000000000..31011253d --- /dev/null +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/config/WebConfig.kt @@ -0,0 +1,20 @@ +package com.egm.stellio.subscription.config + +import org.springframework.context.annotation.Configuration +import org.springframework.http.codec.ServerCodecConfigurer +import org.springframework.web.reactive.config.EnableWebFlux +import org.springframework.web.reactive.config.PathMatchConfigurer +import org.springframework.web.reactive.config.WebFluxConfigurer + +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + configurer.defaultCodecs().enableLoggingRequestDetails(true) + } + + override fun configurePathMatching(configurer: PathMatchConfigurer) { + configurer.setUseTrailingSlashMatch(true) + } +} diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt index 24e2677ac..7f0d73401 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/service/SubscriptionService.kt @@ -27,6 +27,7 @@ import com.egm.stellio.subscription.utils.execute import com.jayway.jsonpath.JsonPath.read import io.r2dbc.postgresql.codec.Json import io.r2dbc.spi.Row +import io.r2dbc.spi.RowMetadata import kotlinx.coroutines.reactive.awaitFirst import org.slf4j.LoggerFactory import org.springframework.data.r2dbc.core.R2dbcEntityTemplate @@ -272,7 +273,12 @@ class SubscriptionService( } listOf( - "subscriptionName", "description", "watchedAttributes", "timeInterval", "q", "isActive", + "subscriptionName", + "description", + "watchedAttributes", + "timeInterval", + "q", + "isActive", "modifiedAt" ).contains(it.key) -> { val columnName = it.key.toSqlColumnName() @@ -422,7 +428,7 @@ class SubscriptionService( } } - fun delete(subscriptionId: URI): Mono = + fun delete(subscriptionId: URI): Mono = r2dbcEntityTemplate.delete( query(where("id").`is`(subscriptionId)), Subscription::class.java @@ -569,7 +575,7 @@ class SubscriptionService( subscription: Subscription, notification: Notification, success: Boolean - ): Mono { + ): Mono { val subscriptionStatus = if (success) NotificationParams.StatusType.OK.name else NotificationParams.StatusType.FAILED.name val lastStatusName = if (success) "last_success" else "last_failure" @@ -585,7 +591,7 @@ class SubscriptionService( ) } - private var rowToSubscription: ((Row) -> Subscription) = { row -> + private var rowToSubscription: ((Row, RowMetadata) -> Subscription) = { row, rowMetadata -> Subscription( id = row.get("sub_id", String::class.java)!!.toUri(), type = row.get("sub_type", String::class.java)!!, @@ -604,7 +610,7 @@ class SubscriptionService( type = row.get("entity_type", String::class.java)!! ) ), - geoQ = rowToGeoQuery(row), + geoQ = rowToGeoQuery(row, rowMetadata), notification = NotificationParams( attributes = row.get("notif_attributes", String::class.java)?.split(",").orEmpty(), format = NotificationParams.FormatType.valueOf(row.get("notif_format", String::class.java)!!), @@ -624,7 +630,7 @@ class SubscriptionService( ) } - private var rowToRawSubscription: ((Row) -> Subscription) = { row -> + private var rowToRawSubscription: ((Row, RowMetadata) -> Subscription) = { row, _ -> Subscription( id = row.get("sub_id", String::class.java)!!.toUri(), type = row.get("sub_type", String::class.java)!!, @@ -649,7 +655,7 @@ class SubscriptionService( ) } - private var rowToGeoQuery: ((Row) -> GeoQuery?) = { row -> + private var rowToGeoQuery: ((Row, RowMetadata) -> GeoQuery?) = { row, _ -> if (row.get("georel", String::class.java) != null) GeoQuery( georel = row.get("georel", String::class.java)!!, @@ -662,15 +668,15 @@ class SubscriptionService( null } - private var matchesGeoQuery: ((Row) -> Boolean) = { row -> + private var matchesGeoQuery: ((Row, RowMetadata) -> Boolean) = { row, _ -> row.get("match", Object::class.java).toString() == "true" } - private var rowToSubscriptionCount: ((Row) -> Int) = { row -> + private var rowToSubscriptionCount: ((Row, RowMetadata) -> Int) = { row, _ -> row.get("count", Integer::class.java)!!.toInt() } - private var rowToSub: (Row) -> String = { row -> + private var rowToSub: (Row, RowMetadata) -> String = { row, _ -> row.get("sub", String::class.java)!! } diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/utils/DBUtils.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/utils/DBQueryUtils.kt similarity index 100% rename from subscription-service/src/main/kotlin/com/egm/stellio/subscription/utils/DBUtils.kt rename to subscription-service/src/main/kotlin/com/egm/stellio/subscription/utils/DBQueryUtils.kt diff --git a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt index d4fb1c45d..7d4920cb4 100644 --- a/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt +++ b/subscription-service/src/main/kotlin/com/egm/stellio/subscription/web/SubscriptionHandler.kt @@ -143,7 +143,6 @@ class SubscriptionHandler( @RequestHeader httpHeaders: HttpHeaders, @RequestBody requestBody: Mono ): ResponseEntity<*> { - return either> { val subscriptionIdUri = subscriptionId.toUri() checkSubscriptionExists(subscriptionIdUri).awaitFirst().bind() diff --git a/subscription-service/src/main/resources/application-docker.properties b/subscription-service/src/main/resources/application-docker.properties index 758994661..04f865838 100644 --- a/subscription-service/src/main/resources/application-docker.properties +++ b/subscription-service/src/main/resources/application-docker.properties @@ -3,6 +3,8 @@ spring.flyway.url = jdbc:postgresql://postgres/stellio_subscription spring.kafka.bootstrap-servers = kafka:9092 +server.error.include-stacktrace = never + spring.devtools.add-properties = false application.authentication.enabled = true diff --git a/subscription-service/src/main/resources/application.properties b/subscription-service/src/main/resources/application.properties index 60761f018..7c4b89559 100644 --- a/subscription-service/src/main/resources/application.properties +++ b/subscription-service/src/main/resources/application.properties @@ -15,8 +15,11 @@ spring.kafka.consumer.auto-offset-reset = earliest # By default, new matching topics are checked every 5 minutes but it can be configured by overriding the following prop # spring.kafka.consumer.properties.metadata.max.age.ms = 1000 -# cf https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#specifying-the-authorization-server +server.error.include-stacktrace = always + +# cf https://docs.spring.io/spring-security/reference/reactive/oauth2/resource-server/jwt.html#_specifying_the_authorization_server spring.security.oauth2.resourceserver.jwt.issuer-uri = https://sso.eglobalmark.com/auth/realms/stellio +# not required, but it avoids tying startup to authorization server's availability spring.security.oauth2.resourceserver.jwt.jwk-set-uri = https://sso.eglobalmark.com/auth/realms/stellio/protocol/openid-connect/certs # Client registration used to get entities from entity-service diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJobTest.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJobTest.kt index 1dd53369f..e0b202e76 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJobTest.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/job/TimeIntervalNotificationJobTest.kt @@ -11,20 +11,16 @@ import com.egm.stellio.subscription.model.Subscription import com.egm.stellio.subscription.service.NotificationService import com.egm.stellio.subscription.service.SubscriptionService import com.egm.stellio.subscription.utils.gimmeRawSubscription -import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock.* -import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.ninjasquad.springmockk.MockkBean import io.mockk.confirmVerified import io.mockk.every import io.mockk.mockkClass import io.mockk.verify import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -34,6 +30,7 @@ import org.springframework.test.context.TestPropertySource import reactor.core.publisher.Mono @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [TimeIntervalNotificationJob::class]) +@WireMockTest(httpPort = 8089) @Import(WebClientConfig::class) @ActiveProfiles("test") @TestPropertySource(properties = ["application.authentication.enabled=false"]) @@ -48,26 +45,6 @@ class TimeIntervalNotificationJobTest { @MockkBean private lateinit var subscriptionService: SubscriptionService - private lateinit var wireMockServer: WireMockServer - - @BeforeAll - fun beforeAll() { - wireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().port(8089)) - wireMockServer.start() - // If not using the default port, we need to instruct explicitly the client (quite redundant) - configureFor(8089) - } - - @AfterEach - fun resetWiremock() { - reset() - } - - @AfterAll - fun afterAll() { - wireMockServer.stop() - } - @Test fun `it should compose the query string used to get matching entities`() { val entities = setOf( diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt index 1162ef221..59b6b8106 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/NotificationServiceTests.kt @@ -11,22 +11,16 @@ import com.egm.stellio.subscription.model.EndpointInfo import com.egm.stellio.subscription.model.NotificationParams import com.egm.stellio.subscription.model.NotificationParams.FormatType import com.egm.stellio.subscription.utils.gimmeRawSubscription -import com.github.tomakehurst.wiremock.WireMockServer -import com.github.tomakehurst.wiremock.client.WireMock.configureFor import com.github.tomakehurst.wiremock.client.WireMock.ok import com.github.tomakehurst.wiremock.client.WireMock.post import com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor -import com.github.tomakehurst.wiremock.client.WireMock.reset import com.github.tomakehurst.wiremock.client.WireMock.stubFor import com.github.tomakehurst.wiremock.client.WireMock.urlMatching import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo import com.github.tomakehurst.wiremock.client.WireMock.verify -import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.github.tomakehurst.wiremock.junit5.WireMockTest import com.ninjasquad.springmockk.MockkBean import io.mockk.* -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @@ -36,6 +30,7 @@ import reactor.core.publisher.Mono import reactor.test.StepVerifier @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [NotificationService::class]) +@WireMockTest(httpPort = 8089) @ActiveProfiles("test") class NotificationServiceTests { @@ -51,8 +46,6 @@ class NotificationServiceTests { @Autowired private lateinit var notificationService: NotificationService - private lateinit var wireMockServer: WireMockServer - private val apiaryId = "urn:ngsi-ld:Apiary:XYZ01" private val contexts = listOf( @@ -63,53 +56,35 @@ class NotificationServiceTests { private val rawEntity = """ - { - "id":"$apiaryId", - "type":"Apiary", - "name":{ - "type":"Property", - "value":"ApiarySophia" - }, - "excludedProp":{ - "type":"Property", - "value":"excluded" - }, - "location": { - "type": "GeoProperty", - "value": { - "type": "Point", - "coordinates": [ - 24.30623, - 60.07966 - ] - } - }, - "@context":[ - "${contexts.joinToString("\",\"")}" - ] - } + { + "id":"$apiaryId", + "type":"Apiary", + "name":{ + "type":"Property", + "value":"ApiarySophia" + }, + "excludedProp":{ + "type":"Property", + "value":"excluded" + }, + "location": { + "type": "GeoProperty", + "value": { + "type": "Point", + "coordinates": [ + 24.30623, + 60.07966 + ] + } + }, + "@context":[ + "${contexts.joinToString("\",\"")}" + ] + } """.trimIndent() private val parsedEntity = expandJsonLdEntity(rawEntity).toNgsiLdEntity() - @BeforeAll - fun beforeAll() { - wireMockServer = WireMockServer(wireMockConfig().port(8089)) - wireMockServer.start() - // If not using the default port, we need to instruct explicitly the client (quite redundant) - configureFor(8089) - } - - @AfterEach - fun resetWiremock() { - reset() - } - - @AfterAll - fun afterAll() { - wireMockServer.stop() - } - @Test fun `it should notify the subscriber and update the subscription`() { val subscription = gimmeRawSubscription() @@ -411,7 +386,9 @@ class NotificationServiceTests { StepVerifier.create( notificationService.callSubscriber( - subscription, apiaryId.toUri(), expandJsonLdEntity(rawEntity) + subscription, + apiaryId.toUri(), + expandJsonLdEntity(rawEntity) ) ) .expectNextMatches { diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt index 7927a115e..46f24dc39 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/service/SubscriptionEventServiceTests.kt @@ -17,10 +17,10 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.kafka.core.KafkaTemplate import org.springframework.test.context.ActiveProfiles -import org.springframework.util.concurrent.SettableListenableFuture import reactor.core.publisher.Mono import java.time.Instant import java.time.ZoneOffset +import java.util.concurrent.CompletableFuture @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = [SubscriptionEventService::class]) @ActiveProfiles("test") @@ -40,7 +40,7 @@ class SubscriptionEventServiceTests { val subscription = gimmeRawSubscription() every { subscriptionService.getById(any()) } answers { Mono.just(subscription) } - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() runBlocking { subscriptionEventService.publishSubscriptionCreateEvent( @@ -60,7 +60,7 @@ class SubscriptionEventServiceTests { val subscriptionUri = "urn:ngsi-ld:Subscription:1".toUri() every { subscriptionService.getById(any()) } answers { Mono.just(subscription) } - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() subscriptionEventService.publishSubscriptionUpdateEvent( null, @@ -77,7 +77,7 @@ class SubscriptionEventServiceTests { suspend fun `it should publish an event of type SUBSCRIPTION_DELETE`() { val subscriptionUri = "urn:ngsi-ld:Subscription:1".toUri() - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() subscriptionEventService.publishSubscriptionDeleteEvent( null, @@ -96,7 +96,7 @@ class SubscriptionEventServiceTests { every { notification.id } returns notificationUri every { notification.type } returns NGSILD_NOTIFICATION_TERM every { notification.notifiedAt } returns Instant.now().atZone(ZoneOffset.UTC) - every { kafkaTemplate.send(any(), any(), any()) } returns SettableListenableFuture() + every { kafkaTemplate.send(any(), any(), any()) } returns CompletableFuture() subscriptionEventService.publishNotificationCreateEvent(null, notification) diff --git a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt index d2d8adc80..583ceb23a 100644 --- a/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt +++ b/subscription-service/src/test/kotlin/com/egm/stellio/subscription/web/SubscriptionHandlerTests.kt @@ -25,6 +25,7 @@ import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest import org.springframework.context.annotation.Import import org.springframework.core.io.ClassPathResource import org.springframework.http.HttpStatus +import org.springframework.http.MediaType import org.springframework.security.test.context.support.WithAnonymousUser import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient @@ -142,6 +143,15 @@ class SubscriptionHandlerTests { verify { subscriptionService.isCreatorOf(subscriptionId, sub) } } + @Test + fun `get subscription by id should return a 406 if the accept header is not correct`() { + webClient.get() + .uri("/ngsi-ld/v1/subscriptions/urn:ngsi-ld:Subscription:1") + .header("Accept", MediaType.APPLICATION_PDF.toString()) + .exchange() + .expectStatus().isEqualTo(HttpStatus.NOT_ACCEPTABLE) + } + @Test fun `create subscription should return a 201 if JSON-LD payload is correct`() { val jsonLdFile = ClassPathResource("/ngsild/subscription.json") @@ -233,6 +243,20 @@ class SubscriptionHandlerTests { ) } + @Test + fun `create subscription should return a 415 if the content type is not correct`() { + val jsonLdFile = ClassPathResource("/ngsild/subscription.json") + + webClient.post() + .uri("/ngsi-ld/v1/subscriptions") + .contentType(MediaType.APPLICATION_PDF) + .bodyValue(jsonLdFile) + .exchange() + .expectStatus().isEqualTo(HttpStatus.UNSUPPORTED_MEDIA_TYPE) + + coVerify { subscriptionService.validateNewSubscription(any()) wasNot Called } + } + @Test fun `create subscription should return a 400 if validation of the subscription fails`() { val jsonLdFile = ClassPathResource("/ngsild/subscription_with_conflicting_timeInterval_watchedAttributes.json") @@ -633,11 +657,11 @@ class SubscriptionHandlerTests { .expectStatus().isNotFound .expectBody().json( """ - { - "detail":"${subscriptionNotFoundMessage(subscriptionId)}", - "type":"https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", - "title":"The referred resource has not been found" - } + { + "detail":"${subscriptionNotFoundMessage(subscriptionId)}", + "type":"https://uri.etsi.org/ngsi-ld/errors/ResourceNotFound", + "title":"The referred resource has not been found" + } """.trimIndent() )