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()
)