From: Ben Zelleröhr Date: Wed, 30 Jul 2025 14:34:35 +0000 (+0200) Subject: refactor(tests): use webTestClient X-Git-Url: https://gerrit.onap.org/r/gitweb?a=commitdiff_plain;h=HEAD;p=portal-ng%2Fpreferences.git refactor(tests): use webTestClient Issue-ID: PORTALNG-150 Change-Id: I2c13d9ac187ee6b826eab21303242b600b078445 Signed-off-by: Ben Zelleröhr --- diff --git a/Dockerfile b/Dockerfile index e1ff0cc..b00c46c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM eclipse-temurin:17 as builder +FROM eclipse-temurin:21 as builder COPY . ./preferences WORKDIR /preferences RUN ./gradlew assemble -FROM eclipse-temurin:17-jre-alpine +FROM eclipse-temurin:21-jre-alpine USER nobody ARG JAR_FILE=/preferences/app/build/libs/app-*.jar COPY --from=builder ${JAR_FILE} app.jar -EXPOSE 9080 -ENTRYPOINT [ "java","-jar","app.jar" ] \ No newline at end of file +EXPOSE 9001 +ENTRYPOINT [ "java","-jar","app.jar" ] diff --git a/app/build.gradle b/app/build.gradle index c0279cd..5647233 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,11 +1,12 @@ plugins { - id 'java' - id 'idea' - id 'application' - id 'io.spring.dependency-management' - id 'org.springframework.boot' - id 'jacoco' - id 'com.gorylenko.gradle-git-properties' + id 'java' + id 'idea' + id 'application' + id 'io.spring.dependency-management' + id 'org.springframework.boot' + id 'jacoco' + id 'com.gorylenko.gradle-git-properties' + id 'com.diffplug.spotless' } def appVersion = getAppVersion() @@ -13,14 +14,14 @@ group = 'org.onap' version = appVersion springBoot { - buildInfo { - properties { - artifact = "onap-portal-ng-preferences" - version = appVersion - group = "org.onap.portalng" - name = "Portal-ng user preferences service" - } - } + buildInfo { + properties { + artifact = "onap-portal-ng-preferences" + version = appVersion + group = "org.onap.portalng" + name = "Portal-ng user preferences service" + } + } } application { @@ -28,71 +29,67 @@ application { } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() - maven { - url = "https://plugins.gradle.org/m2/" - } + mavenCentral() + maven { + url = "https://plugins.gradle.org/m2/" + } } ext { problemVersion = '0.27.1' - logstashLogbackVersion = '8.1' - springCloudWiremockVersion = '4.1.5' - micrometerVersion = '1.0.0' - liquibaseCoreVersion = '4.31.1' + swaggerAnnotationsVersion = '2.2.34' } dependencies { - implementation project(':openapi') - implementation 'org.springframework.boot:spring-boot-starter-actuator' - implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation "org.zalando:problem:$problemVersion" - implementation "net.logstash.logback:logstash-logback-encoder:$logstashLogbackVersion" - - implementation "org.liquibase:liquibase-core:$liquibaseCoreVersion" - implementation 'org.postgresql:postgresql' - - implementation 'io.micrometer:micrometer-tracing' - implementation 'io.micrometer:micrometer-tracing-bridge-otel' - implementation 'io.opentelemetry:opentelemetry-exporter-zipkin' - implementation 'io.micrometer:micrometer-registry-prometheus' - implementation 'org.apache.commons:commons-lang3:3.17.0' - - compileOnly 'org.projectlombok:lombok' - - developmentOnly 'org.springframework.boot:spring-boot-devtools' - - annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' - annotationProcessor 'org.projectlombok:lombok' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'io.projectreactor:reactor-test' - testImplementation 'io.rest-assured:rest-assured' - testImplementation "org.springframework.cloud:spring-cloud-contract-wiremock:$springCloudWiremockVersion" - testImplementation "org.testcontainers:postgresql" - testCompileOnly 'org.projectlombok:lombok' - testAnnotationProcessor 'org.projectlombok:lombok' + implementation project(':openapi') + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation "org.zalando:problem:$problemVersion" + + implementation "org.liquibase:liquibase-core" + implementation 'org.postgresql:postgresql' + + implementation 'io.micrometer:micrometer-tracing' + implementation 'io.micrometer:micrometer-tracing-bridge-otel' + implementation 'io.opentelemetry:opentelemetry-exporter-zipkin' + implementation 'io.micrometer:micrometer-registry-prometheus' + + compileOnly 'org.projectlombok:lombok' + compileOnly "io.swagger.core.v3:swagger-annotations-jakarta:$swaggerAnnotationsVersion" + + developmentOnly 'org.springframework.boot:spring-boot-devtools' + + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + annotationProcessor 'org.projectlombok:lombok' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'io.projectreactor:reactor-test' + testImplementation "org.testcontainers:postgresql" + testCompileOnly 'org.projectlombok:lombok' + testCompileOnly "io.swagger.core.v3:swagger-annotations-jakarta:$swaggerAnnotationsVersion" + testAnnotationProcessor 'org.projectlombok:lombok' } test { - useJUnitPlatform() - finalizedBy(jacocoTestReport) + useJUnitPlatform() + finalizedBy(jacocoTestReport) } jacocoTestReport { - reports { - xml.required = true - } + reports { + xml.required = true + } } // avoid generating X.X.X-plain.jar @@ -101,22 +98,28 @@ jar { } def String getAppVersion() { - Properties versionProperties = getVersionProperties() - String major = versionProperties.getProperty('major') - String minor = versionProperties.getProperty('minor') - String patch = versionProperties.getProperty('patch') - return major + '.' + minor + '.' + patch + Properties versionProperties = getVersionProperties() + String major = versionProperties.getProperty('major') + String minor = versionProperties.getProperty('minor') + String patch = versionProperties.getProperty('patch') + return major + '.' + minor + '.' + patch } def Properties getVersionProperties() { - def versionProperties = new Properties() - rootProject.file('version.properties').withInputStream { - versionProperties.load(it) - } - return versionProperties + def versionProperties = new Properties() + rootProject.file('version.properties').withInputStream { + versionProperties.load(it) + } + return versionProperties } gitProperties { - // if .git directory is on the same level as the root project - dotGitDirectory = project.rootProject.layout.projectDirectory.dir(".git") + // if .git directory is on the same level as the root project + dotGitDirectory = project.rootProject.layout.projectDirectory.dir(".git") +} + +spotless { + java { + googleJavaFormat() + } } diff --git a/app/src/main/java/org/onap/portalng/preferences/PreferencesApplication.java b/app/src/main/java/org/onap/portalng/preferences/PreferencesApplication.java index 1bbdddf..7741952 100644 --- a/app/src/main/java/org/onap/portalng/preferences/PreferencesApplication.java +++ b/app/src/main/java/org/onap/portalng/preferences/PreferencesApplication.java @@ -21,8 +21,8 @@ package org.onap.portalng.preferences; -import org.onap.portalng.preferences.logging.LoggerProperties; import org.onap.portalng.preferences.configuration.PreferencesConfig; +import org.onap.portalng.preferences.logging.LoggerProperties; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -31,8 +31,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties @EnableConfigurationProperties({PreferencesConfig.class, LoggerProperties.class}) public class PreferencesApplication { - public static void main(String[] args) { - SpringApplication.run(PreferencesApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(PreferencesApplication.class, args); + } } diff --git a/app/src/main/java/org/onap/portalng/preferences/configuration/BeansConfig.java b/app/src/main/java/org/onap/portalng/preferences/configuration/BeansConfig.java index f35d43c..0ed8673 100644 --- a/app/src/main/java/org/onap/portalng/preferences/configuration/BeansConfig.java +++ b/app/src/main/java/org/onap/portalng/preferences/configuration/BeansConfig.java @@ -21,11 +21,10 @@ package org.onap.portalng.preferences.configuration; +import java.time.Clock; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.time.Clock; - @Configuration public class BeansConfig { @Bean diff --git a/app/src/main/java/org/onap/portalng/preferences/configuration/LogInterceptor.java b/app/src/main/java/org/onap/portalng/preferences/configuration/LogInterceptor.java index a66c5c4..d42e63b 100644 --- a/app/src/main/java/org/onap/portalng/preferences/configuration/LogInterceptor.java +++ b/app/src/main/java/org/onap/portalng/preferences/configuration/LogInterceptor.java @@ -21,6 +21,7 @@ package org.onap.portalng.preferences.configuration; +import java.util.List; import org.onap.portalng.preferences.util.Logger; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -30,12 +31,10 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -import java.util.List; - @Component public class LogInterceptor implements WebFilter { - public static final String EXCHANGE_CONTEXT_ATTRIBUTE = ServerWebExchangeContextFilter.class.getName() - + ".EXCHANGE_CONTEXT"; + public static final String EXCHANGE_CONTEXT_ATTRIBUTE = + ServerWebExchangeContextFilter.class.getName() + ".EXCHANGE_CONTEXT"; @Value("${logger.traceIdHeaderName}") public static String X_REQUEST_ID; @@ -45,13 +44,15 @@ public class LogInterceptor implements WebFilter { List xRequestIdList = exchange.getRequest().getHeaders().get(X_REQUEST_ID); if (xRequestIdList != null && !xRequestIdList.isEmpty()) { String xRequestId = xRequestIdList.get(0); - Logger.requestLog( - exchange.getRequest().getMethod(), exchange.getRequest().getURI()); + Logger.requestLog(exchange.getRequest().getMethod(), exchange.getRequest().getURI()); exchange.getResponse().getHeaders().add(X_REQUEST_ID, xRequestId); - exchange.getResponse().beforeCommit(() -> { - Logger.responseLog(exchange.getResponse().getStatusCode()); - return Mono.empty(); - }); + exchange + .getResponse() + .beforeCommit( + () -> { + Logger.responseLog(exchange.getResponse().getStatusCode()); + return Mono.empty(); + }); } return chain diff --git a/app/src/main/java/org/onap/portalng/preferences/configuration/PreferencesConfig.java b/app/src/main/java/org/onap/portalng/preferences/configuration/PreferencesConfig.java index 1394fd5..32ccf1d 100644 --- a/app/src/main/java/org/onap/portalng/preferences/configuration/PreferencesConfig.java +++ b/app/src/main/java/org/onap/portalng/preferences/configuration/PreferencesConfig.java @@ -21,16 +21,10 @@ package org.onap.portalng.preferences.configuration; -import org.springframework.boot.context.properties.ConfigurationProperties; - import jakarta.validation.constraints.NotBlank; -import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; -@Data +@Validated @ConfigurationProperties("preferences") -public class PreferencesConfig { - - @NotBlank - private final String realm; - -} +public record PreferencesConfig(@NotBlank String realm) {} diff --git a/app/src/main/java/org/onap/portalng/preferences/configuration/SecurityConfig.java b/app/src/main/java/org/onap/portalng/preferences/configuration/SecurityConfig.java index 2f86d55..7c386d7 100644 --- a/app/src/main/java/org/onap/portalng/preferences/configuration/SecurityConfig.java +++ b/app/src/main/java/org/onap/portalng/preferences/configuration/SecurityConfig.java @@ -21,17 +21,16 @@ package org.onap.portalng.preferences.configuration; +import static org.springframework.security.config.Customizer.withDefaults; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.web.server.SecurityWebFilterChain; -/** - * Configures the access control of the API endpoints. - */ +/** Configures the access control of the API endpoints. */ // https://hantsy.github.io/spring-reactive-sample/security/config.html @EnableWebFluxSecurity @Configuration @@ -39,15 +38,18 @@ public class SecurityConfig { @Bean public SecurityWebFilterChain springSecurityWebFilterChain(ServerHttpSecurity http) { - return http - .httpBasic(basic -> basic.disable()) + return http.httpBasic(basic -> basic.disable()) .formLogin(login -> login.disable()) .csrf(csrf -> csrf.disable()) - .cors(Customizer.withDefaults()) - .authorizeExchange(exchange -> exchange - .pathMatchers(HttpMethod.GET, "/actuator/**").permitAll() - .anyExchange().authenticated()) - .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) + .cors(withDefaults()) + .authorizeExchange( + exchange -> + exchange + .pathMatchers(HttpMethod.GET, "/actuator/**") + .permitAll() + .anyExchange() + .authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2.jwt(withDefaults())) .build(); } } diff --git a/app/src/main/java/org/onap/portalng/preferences/controller/PreferencesController.java b/app/src/main/java/org/onap/portalng/preferences/controller/PreferencesController.java index 5241848..6f9b4a1 100644 --- a/app/src/main/java/org/onap/portalng/preferences/controller/PreferencesController.java +++ b/app/src/main/java/org/onap/portalng/preferences/controller/PreferencesController.java @@ -31,7 +31,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ServerWebExchange; - import reactor.core.publisher.Mono; @RestController @@ -45,37 +44,36 @@ public class PreferencesController implements PreferencesApi { @Override public Mono> getPreferences(ServerWebExchange exchange) { - return IdTokenExchange - .extractUserId(exchange) - .flatMap(userid -> preferencesService.getPreferences(userid) - .map(ResponseEntity::ok)) - .onErrorResume(ProblemException.class, ex -> { - Logger.errorLog("user preferences", null, "preferences"); - return Mono.error(ex); - }) + return IdTokenExchange.extractUserId(exchange) + .flatMap(userid -> preferencesService.getPreferences(userid).map(ResponseEntity::ok)) + .onErrorResume( + ProblemException.class, + ex -> { + Logger.errorLog("user preferences", null, "preferences"); + return Mono.error(ex); + }) .onErrorReturn(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); } @Override - public Mono> savePreferences(Mono preferences, - ServerWebExchange exchange) { - return IdTokenExchange - .extractUserId(exchange) - .flatMap(userid -> preferences - .flatMap(pref -> preferencesService - .savePreferences(userid, pref))) + public Mono> savePreferences( + Mono preferences, ServerWebExchange exchange) { + return IdTokenExchange.extractUserId(exchange) + .flatMap( + userid -> preferences.flatMap(pref -> preferencesService.savePreferences(userid, pref))) .map(ResponseEntity::ok) - .onErrorResume(ProblemException.class, ex -> { - Logger.errorLog("user preferences", null, "preferences"); - return Mono.error(ex); - }) + .onErrorResume( + ProblemException.class, + ex -> { + Logger.errorLog("user preferences", null, "preferences"); + return Mono.error(ex); + }) .onErrorReturn(new ResponseEntity<>(HttpStatus.BAD_REQUEST)); } @Override - public Mono> updatePreferences(Mono preferences, - ServerWebExchange exchange) { + public Mono> updatePreferences( + Mono preferences, ServerWebExchange exchange) { return savePreferences(preferences, exchange); } - } diff --git a/app/src/main/java/org/onap/portalng/preferences/entities/PreferencesDto.java b/app/src/main/java/org/onap/portalng/preferences/entities/PreferencesDto.java index 390a720..6a8e3a8 100644 --- a/app/src/main/java/org/onap/portalng/preferences/entities/PreferencesDto.java +++ b/app/src/main/java/org/onap/portalng/preferences/entities/PreferencesDto.java @@ -21,30 +21,22 @@ package org.onap.portalng.preferences.entities; -import lombok.Getter; -import lombok.Setter; - -import java.util.Map; - -import org.hibernate.annotations.JdbcTypeCode; -import org.hibernate.type.SqlTypes; - import com.fasterxml.jackson.databind.JsonNode; - import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; @Entity @Getter @Setter @Table(name = "preferences") public class PreferencesDto { - @Id - private String userId; + @Id private String userId; @JdbcTypeCode(SqlTypes.JSON) private JsonNode properties; - } - diff --git a/app/src/main/java/org/onap/portalng/preferences/exception/ProblemException.java b/app/src/main/java/org/onap/portalng/preferences/exception/ProblemException.java index da7872f..70eae83 100644 --- a/app/src/main/java/org/onap/portalng/preferences/exception/ProblemException.java +++ b/app/src/main/java/org/onap/portalng/preferences/exception/ProblemException.java @@ -21,6 +21,7 @@ package org.onap.portalng.preferences.exception; +import java.net.URI; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -31,8 +32,6 @@ import org.zalando.problem.Problem; import org.zalando.problem.Status; import org.zalando.problem.StatusType; -import java.net.URI; - /** The default preferences exception */ @Data @Builder @@ -49,5 +48,4 @@ public class ProblemException extends AbstractThrowableProblem { @Builder.Default private final String detail = "Please add more details here"; @Builder.Default private final URI instance = null; - } diff --git a/app/src/main/java/org/onap/portalng/preferences/logging/LoggerProperties.java b/app/src/main/java/org/onap/portalng/preferences/logging/LoggerProperties.java index 7bcc512..e1a311f 100644 --- a/app/src/main/java/org/onap/portalng/preferences/logging/LoggerProperties.java +++ b/app/src/main/java/org/onap/portalng/preferences/logging/LoggerProperties.java @@ -1,11 +1,29 @@ +/* + * + * Copyright (c) 2023. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * + */ + package org.onap.portalng.preferences.logging; import java.util.List; - import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("logger") public record LoggerProperties( - String traceIdHeaderName, - Boolean enabled, List excludePaths) { -} \ No newline at end of file + String traceIdHeaderName, Boolean enabled, List excludePaths) {} diff --git a/app/src/main/java/org/onap/portalng/preferences/logging/LoggingHelper.java b/app/src/main/java/org/onap/portalng/preferences/logging/LoggingHelper.java index 0131ec3..1a350f3 100644 --- a/app/src/main/java/org/onap/portalng/preferences/logging/LoggingHelper.java +++ b/app/src/main/java/org/onap/portalng/preferences/logging/LoggingHelper.java @@ -21,14 +21,13 @@ package org.onap.portalng.preferences.logging; +import java.util.Map; +import java.util.function.BiConsumer; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.slf4j.Logger; import org.slf4j.MDC; -import java.util.Map; -import java.util.function.BiConsumer; - @NoArgsConstructor(access = AccessLevel.PRIVATE) public class LoggingHelper { public static void error( diff --git a/app/src/main/java/org/onap/portalng/preferences/logging/ReactiveRequestLoggingFilter.java b/app/src/main/java/org/onap/portalng/preferences/logging/ReactiveRequestLoggingFilter.java index d230162..48deab8 100644 --- a/app/src/main/java/org/onap/portalng/preferences/logging/ReactiveRequestLoggingFilter.java +++ b/app/src/main/java/org/onap/portalng/preferences/logging/ReactiveRequestLoggingFilter.java @@ -21,6 +21,8 @@ package org.onap.portalng.preferences.logging; +import java.time.Duration; +import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; @@ -30,9 +32,6 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; -import java.time.Duration; -import java.time.LocalDateTime; - @Slf4j @Component @RequiredArgsConstructor @@ -79,7 +78,8 @@ public class ReactiveRequestLoggingFilter implements WebFilter { boolean loggingDisabled = loggerProperties.enabled() == null || !loggerProperties.enabled(); boolean urlShouldBeSkipped = - WebExchangeUtils.matchUrlsPatternsToPath(loggerProperties.excludePaths(), exchange.getRequest().getPath().value()); + WebExchangeUtils.matchUrlsPatternsToPath( + loggerProperties.excludePaths(), exchange.getRequest().getPath().value()); return loggingDisabled || urlShouldBeSkipped; } diff --git a/app/src/main/java/org/onap/portalng/preferences/logging/WebExchangeUtils.java b/app/src/main/java/org/onap/portalng/preferences/logging/WebExchangeUtils.java index 6a56065..18b75ae 100644 --- a/app/src/main/java/org/onap/portalng/preferences/logging/WebExchangeUtils.java +++ b/app/src/main/java/org/onap/portalng/preferences/logging/WebExchangeUtils.java @@ -21,16 +21,15 @@ package org.onap.portalng.preferences.logging; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; import lombok.AccessLevel; import lombok.NoArgsConstructor; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.server.ServerWebExchange; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - @NoArgsConstructor(access = AccessLevel.PRIVATE) public class WebExchangeUtils { private static final String DEFAULT_TRACE_ID = "REQUEST_ID_IS_NOT_SET"; diff --git a/app/src/main/java/org/onap/portalng/preferences/repository/PreferencesRepository.java b/app/src/main/java/org/onap/portalng/preferences/repository/PreferencesRepository.java index 537ee7e..69f8ca0 100644 --- a/app/src/main/java/org/onap/portalng/preferences/repository/PreferencesRepository.java +++ b/app/src/main/java/org/onap/portalng/preferences/repository/PreferencesRepository.java @@ -23,6 +23,15 @@ package org.onap.portalng.preferences.repository; import org.onap.portalng.preferences.entities.PreferencesDto; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; +@Repository public interface PreferencesRepository extends JpaRepository { + @Modifying + @Transactional + @Query(value = "TRUNCATE TABLE preferences", nativeQuery = true) + void truncateTable(); } diff --git a/app/src/main/java/org/onap/portalng/preferences/services/PreferencesService.java b/app/src/main/java/org/onap/portalng/preferences/services/PreferencesService.java index 591c05f..86eec37 100644 --- a/app/src/main/java/org/onap/portalng/preferences/services/PreferencesService.java +++ b/app/src/main/java/org/onap/portalng/preferences/services/PreferencesService.java @@ -21,17 +21,14 @@ package org.onap.portalng.preferences.services; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; import org.onap.portalng.preferences.entities.PreferencesDto; import org.onap.portalng.preferences.exception.ProblemException; import org.onap.portalng.preferences.openapi.model.PreferencesApiDto; import org.onap.portalng.preferences.repository.PreferencesRepository; import org.onap.portalng.preferences.util.Logger; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import lombok.RequiredArgsConstructor; import reactor.core.publisher.Mono; @RequiredArgsConstructor @@ -43,9 +40,7 @@ public class PreferencesService { private final ObjectMapper objectMapper; public Mono getPreferences(String userId) { - return Mono.just(repository - .findById(userId) - .orElse(defaultPreferences())) + return Mono.just(repository.findById(userId).orElse(defaultPreferences())) .map(this::toPreferences); } @@ -57,11 +52,12 @@ public class PreferencesService { return Mono.just(repository.save(preferencesDto)) .map(this::toPreferences) - .onErrorResume(ProblemException.class, ex -> { - Logger.errorLog("user prefrences", userId, "preferences"); - return Mono.error(ex); - }); - + .onErrorResume( + ProblemException.class, + ex -> { + Logger.errorLog("user prefrences", userId, "preferences"); + return Mono.error(ex); + }); } private PreferencesApiDto toPreferences(PreferencesDto preferencesDto) { @@ -71,9 +67,8 @@ public class PreferencesService { } /** - * Get a Preferences object that is initialised with an empty string. - * This is a) for convenience to not handle 404 on the consuming side and - * b) for security reasons + * Get a Preferences object that is initialised with an empty string. This is a) for convenience + * to not handle 404 on the consuming side and b) for security reasons * * @return PreferencesDto */ diff --git a/app/src/main/java/org/onap/portalng/preferences/util/IdTokenExchange.java b/app/src/main/java/org/onap/portalng/preferences/util/IdTokenExchange.java index 7751374..9f6d207 100644 --- a/app/src/main/java/org/onap/portalng/preferences/util/IdTokenExchange.java +++ b/app/src/main/java/org/onap/portalng/preferences/util/IdTokenExchange.java @@ -26,26 +26,25 @@ import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** - * Represents a function that handles the - * JWT identity token. - * Use this to check if the incoming requests are authorized to call the given - * endpoint + * Represents a function that handles the JWT identity + * token. Use this to check if the incoming requests are authorized to call the given endpoint */ - public final class IdTokenExchange { public static final String JWT_CLAIM_USERID = "sub"; - private IdTokenExchange() { + private IdTokenExchange() {} - } /** * Extract the userId from the given {@link ServerWebExchange} + * * @param exchange the ServerWebExchange that contains information about the incoming request * @return the id of the user */ public static Mono extractUserId(ServerWebExchange exchange) { - return exchange.getPrincipal().cast(JwtAuthenticationToken.class) + return exchange + .getPrincipal() + .cast(JwtAuthenticationToken.class) .map(auth -> auth.getToken().getClaimAsString(JWT_CLAIM_USERID)); } } diff --git a/app/src/main/java/org/onap/portalng/preferences/util/Logger.java b/app/src/main/java/org/onap/portalng/preferences/util/Logger.java index 546dfae..4d9cfba 100644 --- a/app/src/main/java/org/onap/portalng/preferences/util/Logger.java +++ b/app/src/main/java/org/onap/portalng/preferences/util/Logger.java @@ -22,17 +22,14 @@ package org.onap.portalng.preferences.util; import java.net.URI; - +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatusCode; -import lombok.extern.slf4j.Slf4j; - @Slf4j public class Logger { - private Logger() { - } + private Logger() {} public static void requestLog(HttpMethod methode, URI path) { log.info("Preferences - request - {} {}", methode, path); @@ -43,12 +40,10 @@ public class Logger { } public static void errorLog(String msg, String id, String app) { - log.info( - "Preferences - error - {} {} not found in {}", msg, id, app); + log.info("Preferences - error - {} {} not found in {}", msg, id, app); } - public static void errorLog( - String msg, String id, String app, String errorDetails) { + public static void errorLog(String msg, String id, String app, String errorDetails) { log.info( "Preferences - error - {} {} not found in {} error message: {}", msg, diff --git a/app/src/main/resources/application-local.yml b/app/src/main/resources/application-local.yml index d5a196a..77b5206 100644 --- a/app/src/main/resources/application-local.yml +++ b/app/src/main/resources/application-local.yml @@ -1,19 +1,11 @@ -server: - port: 9001 - address: 0.0.0.0 - spring: - jackson: - serialization: - # needed for serializing objects of type object - FAIL_ON_EMPTY_BEANS: false security: oauth2: resourceserver: jwt: jwk-set-uri: http://localhost:8080/realms/ONAP/protocol/openid-connect/certs #Keycloak Endpoint datasource: - url: jdbc:postgresql://localhost:5441/preferences + url: jdbc:postgresql://localhost:5432/preferences username: postgres password: postgres jpa: @@ -23,16 +15,9 @@ preferences: realm: ONAP management: - endpoints: - web: - exposure: - include: "*" - info: - build: - enabled: true - env: - enabled: true - git: - enabled: true - java: - enabled: true + zipkin: + tracing: + endpoint: http://localhost:9411/api/v2/spans + +logger: + traceIdHeaderName: x-b3-traceid diff --git a/app/src/main/resources/application.yml b/app/src/main/resources/application.yml index 695e4cc..f86f190 100644 --- a/app/src/main/resources/application.yml +++ b/app/src/main/resources/application.yml @@ -1,6 +1,6 @@ server: - port: 9001 - address: 0.0.0.0 + port: 9001 + address: 0.0.0.0 spring: application: @@ -8,7 +8,7 @@ spring: jackson: serialization: # needed for serializing objects of type object - FAIL_ON_EMPTY_BEANS: false + fail-on-empty-beans: false security: oauth2: resourceserver: @@ -34,7 +34,7 @@ spring: change-log: "classpath:/db/changelog.xml" preferences: - realm: ${KEYCLOAK_REALM} + realm: ${KEYCLOAK_REALM} management: endpoints: web: @@ -62,3 +62,10 @@ logger: enabled: true excludePaths: - "/actuator/**" + +logging: + structured: + format: + console: logstash + level: + root: info diff --git a/app/src/main/resources/logback-spring.xml b/app/src/main/resources/logback-spring.xml deleted file mode 100644 index 05503bc..0000000 --- a/app/src/main/resources/logback-spring.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - ${LOGBACK_LEVEL:-info} - - - - - - - - diff --git a/app/src/test/java/org/onap/portalng/preferences/actuator/ActuatorIntegrationTest.java b/app/src/test/java/org/onap/portalng/preferences/ActuatorIntegrationTest.java similarity index 60% rename from app/src/test/java/org/onap/portalng/preferences/actuator/ActuatorIntegrationTest.java rename to app/src/test/java/org/onap/portalng/preferences/ActuatorIntegrationTest.java index e8f22d6..693cea7 100644 --- a/app/src/test/java/org/onap/portalng/preferences/actuator/ActuatorIntegrationTest.java +++ b/app/src/test/java/org/onap/portalng/preferences/ActuatorIntegrationTest.java @@ -19,30 +19,28 @@ * */ -package org.onap.portalng.preferences.actuator; +package org.onap.portalng.preferences; -import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; -import org.onap.portalng.preferences.BaseIntegrationTest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.availability.ApplicationAvailability; import org.springframework.boot.availability.LivenessState; import org.springframework.boot.availability.ReadinessState; +import org.springframework.boot.test.context.SpringBootTest; -class ActuatorIntegrationTest extends BaseIntegrationTest { - - @Autowired private ApplicationAvailability applicationAvailability; +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class AcutatorIntegrationTest { + @Autowired private ApplicationAvailability applicationAvailability; - @Test - void livenessProbeIsAvailable() { - assertThat(applicationAvailability.getLivenessState()).isEqualTo(LivenessState.CORRECT); - } - - @Test - void readinessProbeIsAvailable() { - - assertThat(applicationAvailability.getReadinessState()) - .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC); - } + @Test + void livenessProbeIsAvailable() { + assertEquals(applicationAvailability.getLivenessState(), LivenessState.CORRECT); + } + + @Test + void readinessProbeIsAvailable() { + assertEquals(applicationAvailability.getReadinessState(), ReadinessState.ACCEPTING_TRAFFIC); + } } diff --git a/app/src/test/java/org/onap/portalng/preferences/BaseIntegrationTest.java b/app/src/test/java/org/onap/portalng/preferences/BaseIntegrationTest.java deleted file mode 100644 index 2a6c35a..0000000 --- a/app/src/test/java/org/onap/portalng/preferences/BaseIntegrationTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * - * Copyright (c) 2022. Deutsche Telekom AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * - * - */ - -package org.onap.portalng.preferences; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.tomakehurst.wiremock.client.WireMock; -import com.nimbusds.jose.jwk.JWKSet; -import org.onap.portalng.preferences.util.IdTokenExchange; -import org.onap.portalng.preferences.configuration.PreferencesConfig; -import io.restassured.RestAssured; -import io.restassured.filter.log.RequestLoggingFilter; -import io.restassured.filter.log.ResponseLoggingFilter; -import io.restassured.specification.RequestSpecification; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; - -import java.util.List; -import java.util.UUID; - -/** Base class for all tests that has the common config including port, realm, logging and auth. */ -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@AutoConfigureWireMock(port = 0) -public abstract class BaseIntegrationTest { - - @LocalServerPort protected int port; - @Value("${preferences.realm}") - protected String realm; - - @Autowired protected ObjectMapper objectMapper; - @Autowired private TokenGenerator tokenGenerator; - @Autowired protected PreferencesConfig preferencesConfig; - - @BeforeAll - public static void setup() { - RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); - } - - /** Mocks the OIDC auth flow. */ - @BeforeEach - public void mockAuth() { - WireMock.reset(); - - WireMock.stubFor( - WireMock.get( - WireMock.urlMatching( - "/realms/%s/protocol/openid-connect/certs".formatted(realm))) - .willReturn( - WireMock.aResponse() - .withHeader("Content-Type", JWKSet.MIME_TYPE) - .withBody(tokenGenerator.getJwkSet().toString()))); - - final TokenGenerator.TokenGeneratorConfig config = - TokenGenerator.TokenGeneratorConfig.builder().port(port).realm(realm).sub("test-user").build(); - - WireMock.stubFor( - WireMock.post( - WireMock.urlMatching( - "/realms/%s/protocol/openid-connect/token".formatted(realm))) - .withBasicAuth("test", "test") - .withRequestBody(WireMock.containing("grant_type=client_credentials")) - .willReturn( - WireMock.aResponse() - .withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) - .withBody( - objectMapper - .createObjectNode() - .put("token_type", "bearer") - .put("access_token", tokenGenerator.generateToken(config)) - .put("expires_in", config.getExpireIn().getSeconds()) - .put("refresh_token", tokenGenerator.generateToken(config)) - .put("refresh_expires_in", config.getExpireIn().getSeconds()) - .put("not-before-policy", 0) - .put("session_state", UUID.randomUUID().toString()) - .put("scope", "email profile") - .toString()))); - } - - /** - * Builds an OAuth2 configuration including the roles, port and realm. This config can be used to - * generate OAuth2 access tokens. - * - * @param sub the userId - * @param roles the roles used for RBAC - * @return the OAuth2 configuration - */ - protected TokenGenerator.TokenGeneratorConfig getTokenGeneratorConfig(String sub, List roles) { - return TokenGenerator.TokenGeneratorConfig.builder() - .port(port) - .sub(sub) - .realm(realm) - .roles(roles) - .build(); - } - - /** Get a RequestSpecification that does not have an Identity header. */ - protected RequestSpecification unauthenticatedRequestSpecification() { - return RestAssured.given().port(port); - } - - /** - * Object to store common attributes of requests that are going to be made. Adds an Identity - * header for the onap_admin role to the request. - * @return the definition of the incoming request (northbound) - */ - protected RequestSpecification requestSpecification() { - final String idToken = tokenGenerator.generateToken(getTokenGeneratorConfig("test-user", List.of("foo"))); - - return unauthenticatedRequestSpecification() - .auth() - .preemptive() - .oauth2(idToken) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + idToken); - } - - /** - * Object to store common attributes of requests that are going to be made. Adds an Identity - * header for the onap_admin role to the request. - * @param userId the userId that should be contained in the incoming request - * @return the definition of the incoming request (northbound) - */ - protected RequestSpecification requestSpecification(String userId) { - final String idToken = tokenGenerator.generateToken(getTokenGeneratorConfig(userId, List.of("foo"))); - - return unauthenticatedRequestSpecification() - .auth() - .preemptive() - .oauth2(idToken) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + idToken); - } -} diff --git a/app/src/test/java/org/onap/portalng/preferences/PreferencesControllerIntegrationTest.java b/app/src/test/java/org/onap/portalng/preferences/PreferencesControllerIntegrationTest.java new file mode 100644 index 0000000..5bc13d8 --- /dev/null +++ b/app/src/test/java/org/onap/portalng/preferences/PreferencesControllerIntegrationTest.java @@ -0,0 +1,147 @@ +/* + * + * Copyright (c) 2025. Deutsche Telekom AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + * + */ + +package org.onap.portalng.preferences; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.onap.portalng.preferences.openapi.model.PreferencesApiDto; +import org.onap.portalng.preferences.repository.PreferencesRepository; +import org.onap.portalng.preferences.services.PreferencesService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; +import org.springframework.test.web.reactive.server.WebTestClient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class PreferencesControllerIntegrationTest { + + @Autowired private WebTestClient webTestClient; + @Autowired private ObjectMapper objectMapper; + + @BeforeEach + void setup( + final ApplicationContext context, + @Autowired final PreferencesRepository preferencesRepository) { + webTestClient = + WebTestClient.bindToApplicationContext(context) + .apply(SecurityMockServerConfigurers.springSecurity()) + .configureClient() + .build(); + preferencesRepository.truncateTable(); + } + + @Test + void testAuthenticatedAccess() { + webTestClient + .mutateWith(SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.claim("sub", "user"))) + .get() + .uri("/v1/preferences") + .exchange() + .expectStatus() + .isOk(); + } + + @Test + void testUnauthorizedAccess() { + webTestClient.get().uri("/v1/preferences").exchange().expectStatus().isUnauthorized(); + } + + @Test + void thatDefaultUserPreferencesCanBeRetrieved() throws Exception { + final var prefs = getDefaultPreferencesApiDto(); + webTestClient + .mutateWith(SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.claim("sub", "user"))) + .get() + .uri("/v1/preferences") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(objectMapper.writeValueAsString(prefs)); + } + + @Test + void thatSimpleUserPreferencesCanBeSaved() throws Exception { + final var prefs = getSimplePreferencesApiDto(); + webTestClient + .mutateWith(SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.claim("sub", "user"))) + .post() + .uri("/v1/preferences") + .bodyValue(prefs) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(objectMapper.writeValueAsString(prefs)); + } + + @Test + void thatSimpleUserPreferencesCanBeUpdated() throws Exception { + final var prefs = getSimplePreferencesApiDto(); + webTestClient + .mutateWith(SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.claim("sub", "user"))) + .put() + .uri("/v1/preferences") + .bodyValue(prefs) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(objectMapper.writeValueAsString(prefs)); + } + + @Test + void thatComplexUserPreferencesCanBeRetrieved( + @Autowired final PreferencesService preferencesService) throws Exception { + final var prefs = getComplexPreferencesApiDto(); + preferencesService.savePreferences("user", prefs); + webTestClient + .mutateWith(SecurityMockServerConfigurers.mockJwt().jwt(jwt -> jwt.claim("sub", "user"))) + .get() + .uri("/v1/preferences") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .json(objectMapper.writeValueAsString(prefs)); + } + + private PreferencesApiDto getDefaultPreferencesApiDto() { + return new PreferencesApiDto().properties(null); + } + + private PreferencesApiDto getSimplePreferencesApiDto() throws Exception { + return new PreferencesApiDto() + .properties(objectMapper.readValue("{\"appStarter\":\"appStarterValue\"}", Map.class)); + } + + private PreferencesApiDto getComplexPreferencesApiDto() throws Exception { + return new PreferencesApiDto() + .properties( + objectMapper.readValue( + "{\"appStarter\":\"appStarterValue1\", \"dashboard\":{\"dashboardKey\":\"dashboardValue\"}}", + Map.class)); + } +} diff --git a/app/src/test/java/org/onap/portalng/preferences/TokenGenerator.java b/app/src/test/java/org/onap/portalng/preferences/TokenGenerator.java deleted file mode 100644 index 246aeb2..0000000 --- a/app/src/test/java/org/onap/portalng/preferences/TokenGenerator.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * - * Copyright (c) 2022. Deutsche Telekom AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * - * - */ - -package org.onap.portalng.preferences; - -import java.time.Clock; -import java.time.Duration; -import java.time.Instant; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.UUID; - -import com.nimbusds.jose.JOSEObjectType; -import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSHeader; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.crypto.RSASSASigner; -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.KeyUse; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; -import com.nimbusds.jwt.JWTClaimsSet; -import com.nimbusds.jwt.SignedJWT; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import lombok.Builder; -import lombok.Getter; -import lombok.NonNull; - -@Component -public class TokenGenerator { - - private static final String ROLES_CLAIM = "roles"; - private static final String USERID_CLAIM = "sub"; - - private final Clock clock; - private final RSAKey jwk; - private final JWKSet jwkSet; - private final JWSSigner signer; - - public TokenGenerator(Clock clock) { - try { - this.clock = clock; - jwk = - new RSAKeyGenerator(2048) - .keyUse(KeyUse.SIGNATURE) - .keyID(UUID.randomUUID().toString()) - .generate(); - jwkSet = new JWKSet(jwk); - signer = new RSASSASigner(jwk); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public JWKSet getJwkSet() { - return jwkSet; - } - - public String generateToken(TokenGeneratorConfig config) { - final Instant iat = clock.instant(); - final Instant exp = iat.plus(config.expireIn); - - final JWTClaimsSet claims = - new JWTClaimsSet.Builder() - .jwtID(UUID.randomUUID().toString()) - .subject(UUID.randomUUID().toString()) - .issuer(config.issuer()) - .issueTime(Date.from(iat)) - .expirationTime(Date.from(exp)) - .claim(ROLES_CLAIM, config.getRoles()) - .claim(USERID_CLAIM, config.getSub()) - .build(); - - final SignedJWT jwt = - new SignedJWT( - new JWSHeader.Builder(JWSAlgorithm.RS256) - .keyID(jwk.getKeyID()) - .type(JOSEObjectType.JWT) - .build(), - claims); - - try { - jwt.sign(signer); - } catch (Exception e) { - throw new RuntimeException(e); - } - - return jwt.serialize(); - } - - @Getter - @Builder - public static class TokenGeneratorConfig { - private final int port; - - @NonNull private final String sub; - - @NonNull private final String realm; - - @NonNull @Builder.Default private final Duration expireIn = Duration.ofMinutes(5); - - @Builder.Default private final List roles = Collections.emptyList(); - - public String issuer() { - return "http://localhost:%d/realms/%s".formatted(port, realm); - } - } -} diff --git a/app/src/test/java/org/onap/portalng/preferences/preferences/PreferencesControllerIntegrationTest.java b/app/src/test/java/org/onap/portalng/preferences/preferences/PreferencesControllerIntegrationTest.java deleted file mode 100644 index f2bae1c..0000000 --- a/app/src/test/java/org/onap/portalng/preferences/preferences/PreferencesControllerIntegrationTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * - * Copyright (c) 2022. Deutsche Telekom AG - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * - * - */ - -package org.onap.portalng.preferences.preferences; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.onap.portalng.preferences.BaseIntegrationTest; -import org.onap.portalng.preferences.openapi.model.PreferencesApiDto; -import org.onap.portalng.preferences.services.PreferencesService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; - -import io.restassured.http.ContentType; - -class PreferencesControllerIntegrationTest extends BaseIntegrationTest { - - @Autowired - PreferencesService preferencesService; - - @Test - void thatUserPreferencesCanBeRetrieved() { - // First save a user preference before a GET can run - PreferencesApiDto expectedResponse = new PreferencesApiDto() - .properties("{\"properties\": {\"dashboard\": {\"key1:\": \"value2\"}, \"appStarter\": \"value1\"}}"); - preferencesService - .savePreferences("test-user", expectedResponse) - .block(); - - PreferencesApiDto actualResponse = requestSpecification("test-user") - .given() - .accept(MediaType.APPLICATION_JSON_VALUE) - .when() - .get("/v1/preferences") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .body() - .as(PreferencesApiDto.class); - - assertThat(actualResponse).isNotNull(); - assertThat(actualResponse.getProperties()).isEqualTo(expectedResponse.getProperties()); - } - - @Test - void thatUserPreferencesCanNotBeRetrieved() { - unauthenticatedRequestSpecification() - .given() - .accept(MediaType.APPLICATION_JSON_VALUE) - .contentType(ContentType.JSON) - .when() - .get("/v1/preferences") - .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()); - } - - @Test - void thatUserPreferencesCanBeSaved() { - PreferencesApiDto expectedResponse = new PreferencesApiDto() - .properties(""" - { - "properties": { "appStarter": "value1", - "dashboard": {"key1:" : "value2"} - }\s - }\ - """); - PreferencesApiDto actualResponse = requestSpecification() - .given() - .accept(MediaType.APPLICATION_JSON_VALUE) - .contentType(ContentType.JSON) - .body(expectedResponse) - .when() - .post("/v1/preferences") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .body() - .as(PreferencesApiDto.class); - - assertThat(actualResponse).isNotNull(); - assertThat(actualResponse.getProperties()).isEqualTo(expectedResponse.getProperties()); - } - - @Test - void thatUserPreferencesCanBeUpdated() { - // First save a user preference before a GET can run - PreferencesApiDto initialPreferences = new PreferencesApiDto() - .properties(""" - { - "properties": { "appStarter": "value1", - "dashboard": {"key1:" : "value2"} - }\s - }\ - """); - preferencesService - .savePreferences("test-user", initialPreferences) - .block(); - - PreferencesApiDto expectedResponse = new PreferencesApiDto() - .properties(""" - { - "properties": { "appStarter": "value3", - "dashboard": {"key2:" : "value4"} - }\s - }\ - """); - PreferencesApiDto actualResponse = requestSpecification("test-user") - .given() - .accept(MediaType.APPLICATION_JSON_VALUE) - .contentType(ContentType.JSON) - .body(expectedResponse) - .when() - .put("/v1/preferences") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .body() - .as(PreferencesApiDto.class); - - assertThat(actualResponse).isNotNull(); - assertThat(actualResponse.getProperties()).isEqualTo(expectedResponse.getProperties()); - } - - @Test - void thatUserPreferencesCanNotBeFound() { - - PreferencesApiDto actualResponse = requestSpecification("test-canNotBeFound") - .given() - .accept(MediaType.APPLICATION_JSON_VALUE) - .when() - .get("/v1/preferences") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .body() - .as(PreferencesApiDto.class); - - assertThat(actualResponse).isNotNull(); - assertThat(actualResponse.getProperties()).isNull(); - } -} diff --git a/app/src/test/resources/application.yml b/app/src/test/resources/application.yml index a0e639a..4a40a8f 100644 --- a/app/src/test/resources/application.yml +++ b/app/src/test/resources/application.yml @@ -3,17 +3,20 @@ server: address: 0.0.0.0 spring: + application: + name: preferences-test jackson: serialization: # needed for serializing objects of type object - FAIL_ON_EMPTY_BEANS: false + fail-on-empty-beans: false security: oauth2: resourceserver: jwt: - jwk-set-uri: http://localhost:${wiremock.server.port}/realms/ONAP/protocol/openid-connect/certs #Keycloak Endpoint + # this value needs to be set for SpringBoot Context, but the tests us mockJwt so it doesn't need to be a valid address + jwk-set-uri: http://localhost datasource: - url: jdbc:tc:postgresql:16:///preferences + url: jdbc:tc:postgresql:///preferences username: postgres password: postgres jpa: @@ -34,7 +37,6 @@ spring: preferences: realm: ONAP - management: endpoints: web: @@ -49,9 +51,18 @@ management: enabled: true java: enabled: true + tracing: + enabled: false logger: - traceIdHeaderName: ${TRACE_ID_HEADER_NAME} + traceIdHeaderName: x-b3-traceid enabled: true excludePaths: - "/actuator/**" + +logging: + structured: + format: + console: logstash + level: + root: info diff --git a/app/src/test/resources/logback-spring.xml b/app/src/test/resources/logback-spring.xml deleted file mode 100644 index 05503bc..0000000 --- a/app/src/test/resources/logback-spring.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - ${LOGBACK_LEVEL:-info} - - - - - - - - diff --git a/build.gradle b/build.gradle index 9a90e43..899b991 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // this build.gradle is mainly here to satisfy the Jenkins gradle plugin allprojects { repositories { - mavenCentral() + mavenCentral() } } diff --git a/development/.env b/development/.env index 87c0bbb..5d7e42c 100644 --- a/development/.env +++ b/development/.env @@ -1,15 +1,12 @@ KEYCLOAK_IMAGE=quay.io/keycloak/keycloak -KEYCLOAK_VERSION=22.04 +KEYCLOAK_VERSION=26.3 KEYCLOAK_USER=admin -KEYCLOAK_PASSWORD=password -KEYCLOAK_DB=keycloak -KEYCLOAK_DB_USER=keycloak -KEYCLOAK_DB_PASSWORD=password -POSTGRES_IMAGE=postgres -POSTGRES_VERSION=15rc1 -MONGO_IMAGE=mongo -MONGO_VERSION=latest -MONGO_USER=root -MONGO_PASSWORD=password +KEYCLOAK_PASSWORD=admin +POSTGRES_IMAGE=docker.io/postgres +POSTGRES_VERSION=16 +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +JAEGER_IMAGE=cr.jaegertracing.io/jaegertracing/jaeger +JAEGER_VERSION=2.8.0 diff --git a/development/config/onap-realm.json b/development/config/ONAP-realm.json similarity index 100% rename from development/config/onap-realm.json rename to development/config/ONAP-realm.json diff --git a/development/docker-compose.yml b/development/docker-compose.yml index b08f7d5..08fff78 100644 --- a/development/docker-compose.yml +++ b/development/docker-compose.yml @@ -1,40 +1,27 @@ -version: '3' - -volumes: - postgres_data: - driver: local - services: - postgres: - image: "${POSTGRES_IMAGE}:${POSTGRES_VERSION}" - volumes: - - postgres_data:/var/lib/postgresql/data - environment: - POSTGRES_DB: ${KEYCLOAK_DB} - POSTGRES_USER: ${KEYCLOAK_DB_USER} - POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD} keycloak: - image: "${KEYCLOAK_IMAGE}:${KEYCLOAK_VERSION}" - environment: - DB_VENDOR: POSTGRES - DB_ADDR: postgres - DB_DATABASE: ${KEYCLOAK_DB} - DB_USER: ${KEYCLOAK_DB_USER} - DB_SCHEMA: public - DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD} - KEYCLOAK_USER: ${KEYCLOAK_USER} - KEYCLOAK_PASSWORD: ${KEYCLOAK_PASSWORD} - KEYCLOAK_IMPORT: /config/onap-realm.json - ports: - - 8080:8080 - volumes: - - ./config:/config - depends_on: - - postgres - mongo: - image: "${MONGO_IMAGE}:${MONGO_VERSION}" + command: start-dev --import-realm + image: "${KEYCLOAK_IMAGE}:${KEYCLOAK_VERSION}" + environment: + KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_USER} + KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_PASSWORD} ports: - - 27017:27017 + - 8080:8080 + volumes: + - ./config:/opt/keycloak/data/import + postgres: + image: "${POSTGRES_IMAGE}:${POSTGRES_VERSION}" + ports: + - 5432:5432 environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER} - MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: preferences + jaeger: + image: "${JAEGER_IMAGE}:${JAEGER_VERSION}" + ports: + - '9411:9411' + - '5778:5778' + - '4318:4318' + - '4317:4317' + - '16686:16686' diff --git a/openapi/build.gradle b/openapi/build.gradle index 7c3de0d..fddef12 100644 --- a/openapi/build.gradle +++ b/openapi/build.gradle @@ -1,52 +1,52 @@ plugins { - id 'java' - id 'org.openapi.generator' + id 'java' + id 'org.openapi.generator' } dependencies { - compileOnly 'io.swagger.core.v3:swagger-annotations:2.2.34' - compileOnly 'org.springframework.boot:spring-boot-starter-webflux:3.5.4' - compileOnly 'jakarta.validation:jakarta.validation-api:3.1.1' + compileOnly 'io.swagger.core.v3:swagger-annotations-jakarta:2.2.34' + compileOnly 'org.springframework.boot:spring-boot-starter-webflux:3.5.4' + compileOnly 'jakarta.validation:jakarta.validation-api:3.1.1' } // https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator-gradle-plugin/README.adoc openApiGenerate { - generatorName = "spring" - library = "spring-boot" - inputSpec = "$projectDir/src/main/resources/api/api.yml" - outputDir = "$buildDir/openapi" - configOptions = [ - hideGenerationTimestamp: "true", - openApiNullable: "false", - skipDefaultInterface: "true", - dateLibrary: "java8", - interfaceOnly: "true", - useTags: "true", - useOptional: "true", - reactive: "true", - useSpringBoot3: "true" - ] - generateApiTests = false - generateApiDocumentation = false - generateModelTests = false - generateModelDocumentation = false - invokerPackage = "org.onap.portalng.preferences.openapi" - apiPackage = "org.onap.portalng.preferences.openapi.api" - modelPackage = "org.onap.portalng.preferences.openapi.model" - modelNameSuffix = "ApiDto" + generatorName = "spring" + library = "spring-boot" + inputSpec = "$projectDir/src/main/resources/api/api.yml" + outputDir = "$buildDir/openapi" + configOptions = [ + hideGenerationTimestamp: "true", + openApiNullable: "false", + skipDefaultInterface: "true", + dateLibrary: "java8", + interfaceOnly: "true", + useTags: "true", + useOptional: "true", + reactive: "true", + useSpringBoot3: "true" + ] + generateApiTests = false + generateApiDocumentation = false + generateModelTests = false + generateModelDocumentation = false + invokerPackage = "org.onap.portalng.preferences.openapi" + apiPackage = "org.onap.portalng.preferences.openapi.api" + modelPackage = "org.onap.portalng.preferences.openapi.model" + modelNameSuffix = "ApiDto" } compileJava { - dependsOn tasks.openApiGenerate + dependsOn tasks.openApiGenerate } sourceSets { - main { - java { - srcDirs += file("$buildDir/openapi/src/main/java") - } + main { + java { + srcDirs += file("$buildDir/openapi/src/main/java") } + } } tasks.withType(Test).configureEach { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/openapi/src/main/resources/api/api.yml b/openapi/src/main/resources/api/api.yml index 580119f..9df3327 100644 --- a/openapi/src/main/resources/api/api.yml +++ b/openapi/src/main/resources/api/api.yml @@ -12,8 +12,6 @@ tags: paths: /v1/preferences: get: - security: - - bearerAuth: [] description: Returns user preferences summary: Get user preferences operationId: getPreferences @@ -37,8 +35,6 @@ paths: '502': $ref: '#/components/responses/BadGateway' put: - security: - - bearerAuth: [] description: Updates user preferences summary: Update user preferences operationId: updatePreferences @@ -68,8 +64,6 @@ paths: '502': $ref: '#/components/responses/BadGateway' post: - security: - - bearerAuth: [] description: Save user preferences summary: Save user preferences operationId: savePreferences @@ -196,3 +190,5 @@ components: type: http scheme: bearer bearerFormat: JWT +security: + - bearerAuth: [] diff --git a/settings.gradle b/settings.gradle index 815dbfc..b45244a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,37 +1,38 @@ // Centrally declare plugin versions here pluginManagement { - // https://docs.gradle.org/current/userguide/plugins.html#sec:plugin_version_management - plugins { - id 'io.spring.dependency-management' version '1.1.7' - id 'org.springframework.boot' version '3.4.5' - id 'com.github.hierynomus.license' version '0.16.1' - id 'com.gorylenko.gradle-git-properties' version '2.5.0' - id 'org.openapi.generator' version '7.13.0' - } - // https://docs.gradle.org/current/userguide/plugins.html#sec:custom_plugin_repositories - repositories { - mavenCentral() - gradlePluginPortal() - } + // https://docs.gradle.org/current/userguide/plugins.html#sec:plugin_version_management + plugins { + id 'io.spring.dependency-management' version '1.1.7' + id 'org.springframework.boot' version '3.5.4' + id 'com.github.hierynomus.license' version '0.16.1' + id 'com.gorylenko.gradle-git-properties' version '2.5.2' + id 'org.openapi.generator' version '7.14.0' + id 'com.diffplug.spotless' version '7.2.1' + } + // https://docs.gradle.org/current/userguide/plugins.html#sec:custom_plugin_repositories + repositories { + mavenCentral() + gradlePluginPortal() + } } // This is a preview feature, enable in the future and remove repositories blocks from sub build.gradles // https://docs.gradle.org/current/userguide/declaring_repositories.html#sub:centralized-repository-declaration // dependencyResolutionManagement { -// maven { -// url "${maven_central_url}" -// credentials { -// username = "${artifactory_user}" -// password = "${artifactory_password}" -// } -// } -// maven { -// url "${gradle_plugins_url}" -// credentials { -// username = "${artifactory_user}" -// password = "${artifactory_password}" -// } -// } +// maven { +// url "${maven_central_url}" +// credentials { +// username = "${artifactory_user}" +// password = "${artifactory_password}" +// } +// } +// maven { +// url "${gradle_plugins_url}" +// credentials { +// username = "${artifactory_user}" +// password = "${artifactory_password}" +// } +// } // } rootProject.name = 'preferences'