diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..7307083 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,61 @@ +name: SonarQube Analysis + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + analyze: + name: Build and Analyze + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:18.4 + env: + POSTGRES_USER: vaessl + POSTGRES_PASSWORD: vaessl + POSTGRES_DB: vaessl_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout Code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Set up JDK 25 + uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0 + with: + java-version: "25" + distribution: "zulu" # Alternative distribution options are available. + + - name: Make Gradle Executable + run: chmod +x ./gradlew + working-directory: backend + + - name: Build and Analyze + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + + DB_URL: jdbc:postgresql://postgres:5432/vaessl_test + DB_TEST_URL: jdbc:postgresql://postgres:5432/vaessl_test + DB_USERNAME: vaessl + DB_PASSWORD: vaessl + PG_DRIVER_CLASS_NAME: org.postgresql.Driver + + OPENAI_BASE_URL: https://api.openai.com/v1 + OPENAI_KEY: ${{ secrets.OPENAI_KEY }} + ALLOWED_ORIGINS: http://localhost:3000 + run: ./gradlew build sonar --info + working-directory: backend diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 4560b98..c5abb42 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -1,7 +1,11 @@ +val wiremockVersion = "3.12.0" + plugins { java + jacoco id("org.springframework.boot") version "4.1.0" id("io.spring.dependency-management") version "1.1.7" + id("org.sonarqube") version "7.3.0.8198" } group = "com.vaessl" @@ -13,6 +17,15 @@ java { } } +sonar { + properties { + property("sonar.projectKey", "Vaessl") + property("sonar.projectName", "Vaessl") + property("sonar.coverage.jacoco.xmlReportPaths", + "${layout.buildDirectory.get()}/reports/jacoco/test/jacocoTestReport.xml") + } +} + configurations { compileOnly { extendsFrom(configurations.annotationProcessor.get()) @@ -25,6 +38,7 @@ repositories { extra["springAiVersion"] = "2.0.0" + dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-session-jdbc") @@ -32,17 +46,24 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-webmvc") implementation("org.springframework.ai:spring-ai-starter-model-openai") + compileOnly("org.projectlombok:lombok") + developmentOnly("org.springframework.boot:spring-boot-devtools") + runtimeOnly("org.postgresql:postgresql") + annotationProcessor("org.projectlombok:lombok") + testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test") // testImplementation("org.springframework.boot:spring-boot-starter-security-test") testImplementation("org.springframework.boot:spring-boot-starter-validation-test") testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test") + testImplementation("org.wiremock:wiremock-standalone:$wiremockVersion") + testImplementation("org.springframework.boot:spring-boot-starter-session-jdbc-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") - testImplementation("org.wiremock:wiremock-standalone:3.12.0") - testImplementation("org.springframework.boot:spring-boot-starter-session-jdbc-test")} +} dependencyManagement { imports { @@ -55,5 +76,20 @@ tasks.withType { } tasks.withType { - useJUnitPlatform() + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) + testLogging { + events("failed", "standardError") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + showExceptions = true + showCauses = true + showStackTraces = true + } +} + +tasks.jacocoTestReport { + dependsOn(tasks.withType()) + reports { + xml.required = true + } } diff --git a/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java b/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java index 518e0f9..2a1ecb3 100644 --- a/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java +++ b/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java @@ -1,9 +1,9 @@ package com.vaessl.app.connection; import java.time.Instant; +import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.springframework.stereotype.Service; @@ -18,8 +18,11 @@ public class ConnectionService { private final ConnectionRepository cRepository; public ConnectionService(List providers, ConnectionRepository cRepository) { - this.providerRegistry = providers.stream() - .collect(Collectors.toMap(ConnectionProvider::getServiceType, p -> p)); + Map registry = new EnumMap<>(ServiceType.class); + for (ConnectionProvider provider : providers) { + registry.put(provider.getServiceType(), provider); + } + this.providerRegistry = registry; this.cRepository = cRepository; } @@ -49,7 +52,8 @@ public class ConnectionService { return new LoginResult(saved.getId(), response.expiresAt()); } - public ConnectionStatusResponse getConnectionStatus(ServiceType serviceType, Long connectionId) { + public ConnectionStatusResponse getConnectionStatus(ServiceType serviceType, + Long connectionId) { ConnectionEntity entity = cRepository.findById(connectionId).orElse(null); if (entity == null) return null; @@ -58,7 +62,7 @@ public class ConnectionService { Instant expiresAt = (provider != null) ? provider.getTokenExpiry(entity) : null; boolean connected = expiresAt == null || expiresAt.isAfter(Instant.now()); - return new ConnectionStatusResponse(serviceType.name(), entity.getAppUrl(), entity.getUsername(), - expiresAt, connected); + return new ConnectionStatusResponse(serviceType.name(), entity.getAppUrl(), + entity.getUsername(), expiresAt, connected); } } diff --git a/backend/src/main/java/com/vaessl/app/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/vaessl/app/exception/GlobalExceptionHandler.java index 8019f6a..1328c25 100644 --- a/backend/src/main/java/com/vaessl/app/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/vaessl/app/exception/GlobalExceptionHandler.java @@ -3,7 +3,6 @@ package com.vaessl.app.exception; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ProblemDetail; -import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -24,7 +23,7 @@ public class GlobalExceptionHandler { public ProblemDetail handleEmptyCredentialInput(MethodArgumentNotValidException e) { String defaultMessages = e.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage).collect(Collectors.joining(", ")); + .map(field -> field.getDefaultMessage()).collect(Collectors.joining(", ")); return ProblemDetail.forStatusAndDetail(BAD_REQUEST_EMPTY_FIELDS.getStatus(), BAD_REQUEST_EMPTY_FIELDS.getMessage() + " [" + defaultMessages + "]"); diff --git a/backend/src/main/java/com/vaessl/app/search/SearchProvider.java b/backend/src/main/java/com/vaessl/app/search/SearchProvider.java index ef6196c..8b5d042 100644 --- a/backend/src/main/java/com/vaessl/app/search/SearchProvider.java +++ b/backend/src/main/java/com/vaessl/app/search/SearchProvider.java @@ -14,6 +14,7 @@ public interface SearchProvider extends ServiceProvider { * Executes a search query against the remote service and returns matching results. * * @param request the search request containing the query string, app URL, and user credentials + * @param pageable the Pageable interface * @return a list of Page items matching the query */ Page getSearchResults(SearchRequest request, Pageable pageable); diff --git a/backend/src/main/java/com/vaessl/app/search/SearchService.java b/backend/src/main/java/com/vaessl/app/search/SearchService.java index a5314c7..4717205 100644 --- a/backend/src/main/java/com/vaessl/app/search/SearchService.java +++ b/backend/src/main/java/com/vaessl/app/search/SearchService.java @@ -1,8 +1,8 @@ package com.vaessl.app.search; +import java.util.EnumMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -16,8 +16,12 @@ public class SearchService { private final Map providerRegistry; public SearchService(List providers) { - this.providerRegistry = Map.copyOf(providers.stream() - .collect(Collectors.toMap(SearchProvider::getServiceType, p -> p))); + Map registry = new EnumMap<>(ServiceType.class); + for (SearchProvider provider : providers) { + registry.put(provider.getServiceType(), provider); + } + this.providerRegistry = registry; + } /**