diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8ae01a9 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,49 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Java CI + +on: + push: + branches: [ "master" ] + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build & test with Maven + run: mvn -B clean test + - name: Sonar analysis + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=Coreoz_Plume-showcase -Dsonar.organization=coreoz diff --git a/pom.xml b/pom.xml index b9c350e..137a10d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,15 +1,13 @@ - + 4.0.0 com.coreoz - plume-demo-admin + plume-showcase 0.0.1-SNAPSHOT jar - plume-demo-admin + plume-showcase UTF-8 @@ -225,5 +223,4 @@ - diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..464c796 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,23 @@ +sonar.projectKey=plume-showcase +sonar.projectName=plume-showcase +sonar.projectVersion=1.0 +sonar.sourceEncoding=UTF-8 +sonar.sources=src + +# Java +sonar.java.source=17 +sonar.exclusions=src/main/java/com/coreoz/db/generated/*,\ + src/main/java/com/coreoz/db/QuerydslGenerator.java + +# Configure Lombok if needed +# sonar.java.libraries=/home/coreoz/lombok-1.18.30.jar + +#Tests +# Disable some rules on some files +sonar.issue.ignore.multicriteria=j1,j2 +#No literal duplication tests +sonar.issue.ignore.multicriteria.j1.ruleKey=squid:S1192 +sonar.issue.ignore.multicriteria.j1.resourceKey=src/test/**/* +#No method name compliance for tests +sonar.issue.ignore.multicriteria.j2.ruleKey=squid:S00100 +sonar.issue.ignore.multicriteria.j2.resourceKey=src/test/**/* diff --git a/src/main/java/com/coreoz/WebApplication.java b/src/main/java/com/coreoz/WebApplication.java index f970161..9f9eef2 100644 --- a/src/main/java/com/coreoz/WebApplication.java +++ b/src/main/java/com/coreoz/WebApplication.java @@ -1,30 +1,27 @@ package com.coreoz; -import java.time.Duration; -import java.util.concurrent.TimeUnit; - -import com.coreoz.plume.admin.services.scheduler.LogApiScheduledJobs; -import org.glassfish.grizzly.GrizzlyFuture; -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.jersey.server.ResourceConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.coreoz.db.InitializeDatabase; +import com.coreoz.db.DatabaseInitializer; import com.coreoz.guice.ApplicationModule; import com.coreoz.jersey.GrizzlySetup; +import com.coreoz.plume.admin.services.scheduler.LogApiScheduledJobs; import com.coreoz.plume.jersey.guice.JerseyGuiceFeature; import com.coreoz.wisp.Scheduler; import com.google.inject.Guice; import com.google.inject.Injector; import com.google.inject.Stage; +import lombok.extern.slf4j.Slf4j; +import org.glassfish.grizzly.GrizzlyFuture; +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.jersey.server.ResourceConfig; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; /** * The application entry point, where it all begins. */ +@Slf4j public class WebApplication { - private static final Logger logger = LoggerFactory.getLogger(WebApplication.class); - // maximal waiting time for the last process to execute after the JVM received a kill signal public static final Duration GRACEFUL_SHUTDOWN_TIMEOUT = Duration.ofSeconds(60); @@ -36,7 +33,7 @@ public static void main(String[] args) { Injector injector = Guice.createInjector(Stage.PRODUCTION, new ApplicationModule()); // initialize database - injector.getInstance(InitializeDatabase.class).setup(); + injector.getInstance(DatabaseInitializer.class).setup(); // schedule jobs // configure logApi @@ -66,20 +63,20 @@ public static void main(String[] args) { } } - private static void addShutDownListener(HttpServer httpServer, Scheduler scheduler) { Runtime.getRuntime().addShutdownHook(new Thread( () -> { logger.info("Stopping signal received, shutting down server and scheduler..."); GrizzlyFuture grizzlyServerShutdownFuture = httpServer.shutdown(GRACEFUL_SHUTDOWN_TIMEOUT.toSeconds(), TimeUnit.SECONDS); - try { - logger.info("Waiting for server to shut down... Shutdown timeout is {} seconds", GRACEFUL_SHUTDOWN_TIMEOUT.toSeconds()); - scheduler.gracefullyShutdown(GRACEFUL_SHUTDOWN_TIMEOUT); - grizzlyServerShutdownFuture.get(); - } catch(Exception e) { - logger.error("Error while shutting down server.", e); - } - logger.info("Server and scheduler stopped."); + try { + logger.info("Waiting for server to shut down... Shutdown timeout is {} seconds", GRACEFUL_SHUTDOWN_TIMEOUT.toSeconds()); + scheduler.gracefullyShutdown(GRACEFUL_SHUTDOWN_TIMEOUT); + grizzlyServerShutdownFuture.get(); + logger.info("Server and scheduler stopped."); + } catch(Exception e) { + logger.error("Error while shutting down server.", e); + Thread.currentThread().interrupt(); + } }, "shutdownHook" )); diff --git a/src/main/java/com/coreoz/db/InitializeDatabase.java b/src/main/java/com/coreoz/db/DatabaseInitializer.java similarity index 84% rename from src/main/java/com/coreoz/db/InitializeDatabase.java rename to src/main/java/com/coreoz/db/DatabaseInitializer.java index f80561c..8f12be2 100644 --- a/src/main/java/com/coreoz/db/InitializeDatabase.java +++ b/src/main/java/com/coreoz/db/DatabaseInitializer.java @@ -10,12 +10,12 @@ * Initialize the H2 database with SQL scripts placed in src/main/resources/db/migration */ @Singleton -public class InitializeDatabase { +public class DatabaseInitializer { private final DataSource dataSource; @Inject - public InitializeDatabase(DataSource dataSource) { + public DatabaseInitializer(DataSource dataSource) { this.dataSource = dataSource; } @@ -27,5 +27,4 @@ public void setup() { .load() .migrate(); } - } diff --git a/src/main/java/com/coreoz/db/QuerydslGenerator.java b/src/main/java/com/coreoz/db/QuerydslGenerator.java index bdda448..ddc533e 100644 --- a/src/main/java/com/coreoz/db/QuerydslGenerator.java +++ b/src/main/java/com/coreoz/db/QuerydslGenerator.java @@ -22,14 +22,16 @@ import com.querydsl.sql.types.JSR310LocalTimeType; import com.querydsl.sql.types.JSR310ZonedDateTimeType; import com.querydsl.sql.types.Type; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; /** * Generate Querydsl classes for the database layer. * * Run the {@link #main(String...)} method from your IDE to regenerate Querydsl classes. */ +@Slf4j public class QuerydslGenerator { - private static final String TABLES_PREFIX = "SWC_"; public static void main(String... args) { @@ -77,17 +79,13 @@ public String getDefaultVariableName(EntityType entityType) { try { exporter.export(connection.getMetaData()); } catch (SQLException e) { - e.printStackTrace(); + logger.error("Querydsl database objects generation failed", e); } }); } + @SneakyThrows private static Type classType(Class classType) { - try { - return (Type) classType.getConstructor().newInstance(); - } catch (Exception e) { - throw new RuntimeException(e); - } + return (Type) classType.getConstructor().newInstance(); } - } diff --git a/src/main/java/com/coreoz/services/configuration/ConfigurationService.java b/src/main/java/com/coreoz/services/configuration/ConfigurationService.java index d018ed8..0b0ff6c 100644 --- a/src/main/java/com/coreoz/services/configuration/ConfigurationService.java +++ b/src/main/java/com/coreoz/services/configuration/ConfigurationService.java @@ -7,7 +7,6 @@ @Singleton public class ConfigurationService { - private final Config config; @Inject @@ -19,13 +18,11 @@ public String hello() { return config.getString("hello"); } - public String swaggerAccessUsername() { - return config.getString("swagger.access.username"); - } - - public String swaggerAccessPassword() { - return config.getString("swagger.access.password"); - } + public String internalApiAuthUsername() { + return config.getString("internal-api.auth-username"); + } + public String internalApiAuthPassword() { + return config.getString("internal-api.auth-password"); + } } - diff --git a/src/main/java/com/coreoz/webservices/admin/permissions/ProjectAdminPermissionService.java b/src/main/java/com/coreoz/webservices/admin/permissions/ProjectAdminPermissionService.java index 27496b0..ed1742a 100644 --- a/src/main/java/com/coreoz/webservices/admin/permissions/ProjectAdminPermissionService.java +++ b/src/main/java/com/coreoz/webservices/admin/permissions/ProjectAdminPermissionService.java @@ -1,15 +1,13 @@ package com.coreoz.webservices.admin.permissions; -import java.util.Set; - -import javax.inject.Inject; -import javax.inject.Singleton; - import com.coreoz.plume.admin.services.permission.LogApiAdminPermissions; import com.coreoz.plume.admin.services.permission.SystemAdminPermissions; import com.coreoz.plume.admin.services.permissions.AdminPermissionService; import com.coreoz.plume.admin.services.permissions.AdminPermissions; -import com.google.common.collect.ImmutableSet; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.Set; @Singleton public class ProjectAdminPermissionService implements AdminPermissionService { @@ -18,7 +16,7 @@ public class ProjectAdminPermissionService implements AdminPermissionService { @Inject public ProjectAdminPermissionService() { - this.permissionsAvailable = ImmutableSet.of( + this.permissionsAvailable = Set.of( AdminPermissions.MANAGE_USERS, AdminPermissions.MANAGE_ROLES, LogApiAdminPermissions.MANAGE_API_LOGS, diff --git a/src/main/java/com/coreoz/webservices/internal/InternalApiAuthenticator.java b/src/main/java/com/coreoz/webservices/internal/InternalApiAuthenticator.java new file mode 100644 index 0000000..edb6460 --- /dev/null +++ b/src/main/java/com/coreoz/webservices/internal/InternalApiAuthenticator.java @@ -0,0 +1,25 @@ +package com.coreoz.webservices.internal; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import com.coreoz.plume.jersey.security.basic.BasicAuthenticator; +import com.coreoz.services.configuration.ConfigurationService; + +@Singleton +public class InternalApiAuthenticator { + private final BasicAuthenticator basicAuthenticator; + + @Inject + public InternalApiAuthenticator(ConfigurationService configurationService) { + this.basicAuthenticator = BasicAuthenticator.fromSingleCredentials( + configurationService.internalApiAuthUsername(), + configurationService.internalApiAuthPassword(), + "API plume-showcase" + ); + } + + public BasicAuthenticator get() { + return this.basicAuthenticator; + } +} diff --git a/src/main/java/com/coreoz/webservices/internal/MonitoringWs.java b/src/main/java/com/coreoz/webservices/internal/MonitoringWs.java index c2dbb18..fb0261d 100644 --- a/src/main/java/com/coreoz/webservices/internal/MonitoringWs.java +++ b/src/main/java/com/coreoz/webservices/internal/MonitoringWs.java @@ -13,11 +13,11 @@ import javax.ws.rs.core.MediaType; import com.codahale.metrics.Metric; +import com.coreoz.plume.db.transaction.TransactionManager; import com.coreoz.plume.jersey.monitoring.utils.health.HealthCheckBuilder; import com.coreoz.plume.jersey.monitoring.utils.health.beans.HealthStatus; import com.coreoz.plume.jersey.monitoring.utils.info.ApplicationInfoProvider; -import com.coreoz.plume.db.transaction.TransactionManager; import com.coreoz.plume.jersey.monitoring.utils.info.beans.ApplicationInfo; import com.coreoz.plume.jersey.monitoring.utils.metrics.MetricsCheckBuilder; import com.coreoz.plume.jersey.security.basic.BasicAuthenticator; @@ -36,7 +36,11 @@ public class MonitoringWs { private final BasicAuthenticator basicAuthenticator; @Inject - public MonitoringWs(ApplicationInfoProvider applicationInfoProvider, TransactionManager transactionManager) { + public MonitoringWs( + ApplicationInfoProvider applicationInfoProvider, + TransactionManager transactionManager, + InternalApiAuthenticator apiAuthenticator + ) { this.applicationInfo = applicationInfoProvider.get(); // Registering health checks this.healthStatusProvider = new HealthCheckBuilder() @@ -49,11 +53,7 @@ public MonitoringWs(ApplicationInfoProvider applicationInfoProvider, Transaction .build(); // Require authentication to access monitoring endpoints - this.basicAuthenticator = BasicAuthenticator.fromSingleCredentials( - "plume", - "rocks", - "Plume showcase" - ); + this.basicAuthenticator = apiAuthenticator.get(); } @GET diff --git a/src/main/java/com/coreoz/webservices/internal/SwaggerWs.java b/src/main/java/com/coreoz/webservices/internal/SwaggerWs.java index a64196c..e0b70dc 100644 --- a/src/main/java/com/coreoz/webservices/internal/SwaggerWs.java +++ b/src/main/java/com/coreoz/webservices/internal/SwaggerWs.java @@ -14,12 +14,9 @@ import com.coreoz.plume.jersey.security.basic.BasicAuthenticator; import com.coreoz.plume.jersey.security.permission.PublicApi; -import com.coreoz.services.configuration.ConfigurationService; -import com.fasterxml.jackson.core.JsonProcessingException; import io.swagger.v3.core.util.Yaml; import io.swagger.v3.jaxrs2.integration.JaxrsOpenApiContextBuilder; -import io.swagger.v3.oas.integration.OpenApiConfigurationException; import io.swagger.v3.oas.integration.SwaggerConfiguration; import io.swagger.v3.oas.integration.api.OpenApiContext; import io.swagger.v3.oas.models.OpenAPI; @@ -30,12 +27,13 @@ @Singleton @PublicApi public class SwaggerWs { - private final String swaggerDefinition; private final BasicAuthenticator basicAuthenticator; + @SneakyThrows @Inject - public SwaggerWs(ConfigurationService configurationService) throws OpenApiConfigurationException { + public SwaggerWs(InternalApiAuthenticator apiAuthenticator) { + // Basic configuration SwaggerConfiguration openApiConfig = new SwaggerConfiguration() .resourcePackages(Set.of("com.coreoz.webservices.api")) .sortOutput(true) @@ -45,33 +43,26 @@ public SwaggerWs(ConfigurationService configurationService) throws OpenApiConfig .description("API plume-showcase") ))); + // Generation of the OpenApi object OpenApiContext context = new JaxrsOpenApiContextBuilder<>() .openApiConfiguration(openApiConfig) .buildContext(true); + // the OpenAPI object can be changed to add security definition + // or to alter the generated mapping OpenAPI openApi = context.read(); // serialization of the Swagger definition - try { - this.swaggerDefinition = Yaml.mapper().writeValueAsString(openApi); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } + this.swaggerDefinition = Yaml.mapper().writeValueAsString(openApi); // require authentication to access the API documentation - this.basicAuthenticator = BasicAuthenticator.fromSingleCredentials( - configurationService.swaggerAccessUsername(), - configurationService.swaggerAccessPassword(), - "API plume-showcase" - ); + this.basicAuthenticator = apiAuthenticator.get(); } @Produces(MediaType.APPLICATION_JSON) @GET - public String get(@Context ContainerRequestContext requestContext) throws JsonProcessingException { + public String get(@Context ContainerRequestContext requestContext) { basicAuthenticator.requireAuthentication(requestContext); return swaggerDefinition; } - } - diff --git a/src/test/java/com/coreoz/SampleTest.java b/src/test/java/com/coreoz/SampleTest.java index 23469f5..a3717d5 100644 --- a/src/test/java/com/coreoz/SampleTest.java +++ b/src/test/java/com/coreoz/SampleTest.java @@ -3,9 +3,21 @@ import org.assertj.core.api.Assertions; import org.junit.Test; +/** + * A unit test sample. + * + * Unit tests are a great tool for: + * - Testing exhaustively a function by changing all the parameters to verify that is fully respects its specification + * - Testing a function that does not have a lot of dependencies + * + * To test something that has interactions with the database, or not only one function but a chain of services, + * integration tests are preferred. See {@link SampleIntegrationTest} for an example. + * + * Once there are other unit tests in the project, this sample should be deleted. + */ public class SampleTest { @Test public void methodToTest__test_scenario_description() { - Assertions.assertThat(1).isEqualTo(1); + Assertions.assertThat(1 + 1).isEqualTo(2); } } diff --git a/src/test/java/com/coreoz/guice/TestModule.java b/src/test/java/com/coreoz/guice/TestModule.java index c676812..f05601a 100644 --- a/src/test/java/com/coreoz/guice/TestModule.java +++ b/src/test/java/com/coreoz/guice/TestModule.java @@ -5,15 +5,21 @@ import com.google.inject.AbstractModule; import com.google.inject.util.Modules; +/** + * The Guice module that will be used for integration tests. + * + * In this module, it is possible to override the behaviors of some services as it is shown with the {@link TimeProvider} + * module. + */ public class TestModule extends AbstractModule { - @Override - protected void configure() { - install(Modules.override(new ApplicationModule()).with(new AbstractModule() { - @Override - protected void configure() { - bind(TimeProvider.class).to(TimeProviderForTest.class); - } - })); + @Override + protected void configure() { + install(Modules.override(new ApplicationModule()).with(new AbstractModule() { + @Override + protected void configure() { + bind(TimeProvider.class).to(TestableTimeProvider.class); + } + })); install(new GuiceDbTestModule()); - } + } } diff --git a/src/test/java/com/coreoz/guice/TestableTimeProvider.java b/src/test/java/com/coreoz/guice/TestableTimeProvider.java new file mode 100644 index 0000000..732a23e --- /dev/null +++ b/src/test/java/com/coreoz/guice/TestableTimeProvider.java @@ -0,0 +1,71 @@ +package com.coreoz.guice; + +import com.coreoz.plume.services.time.TimeProvider; + +import javax.inject.Singleton; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +/** + * Override the default {@link TimeProvider} for testing purposes. + * This adds the possibility to change how time flows during a test. + * This works only for code that relies on the {@link TimeProvider} + */ +@Singleton +public class TestableTimeProvider implements TimeProvider { + private Clock clock; + + public TestableTimeProvider() { + this.clock = Clock.systemDefaultZone(); + } + + /** + * Returns the current clock used + */ + @Override + public Clock clock() { + return clock; + } + + /** + * Changes the current clock used. This is generally a temporary measure that should be reverted. + * See {@link #executeWithClock(Clock, Runnable)} for usage + * @param newClock The new clock to use + */ + public void changeClock(Clock newClock) { + this.clock = newClock; + } + + /** + * Execute a function with a custom clock. If unsure, use {@link #executeWithInstant(Instant, Runnable)} or {@link #executeWithConstantTime(Runnable)} instead + * @param newClock The custom clock + * @param toExecuteWithClock The function to execute + */ + public void executeWithClock(Clock newClock, Runnable toExecuteWithClock) { + Clock oldClock = this.clock; + changeClock(newClock); + toExecuteWithClock.run(); + changeClock(oldClock); + } + + /** + * Execute a function for which for time does not change + * @param fixedInstantForExecution The instant that will be used to execute the function + * @param toExecuteWithInstant The function to execute + */ + public void executeWithInstant(Instant fixedInstantForExecution, Runnable toExecuteWithInstant) { + executeWithClock( + Clock.fixed(fixedInstantForExecution, ZoneId.systemDefault()), + toExecuteWithInstant + ); + } + + /** + * Execute a function for which for time does not change + * @param toExecuteWithConstantTime The function to execute + */ + public void executeWithConstantTime(Runnable toExecuteWithConstantTime) { + executeWithInstant(Instant.now(), toExecuteWithConstantTime); + } +} diff --git a/src/test/java/com/coreoz/guice/TimeProviderForTest.java b/src/test/java/com/coreoz/guice/TimeProviderForTest.java deleted file mode 100644 index 7b22cc4..0000000 --- a/src/test/java/com/coreoz/guice/TimeProviderForTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.coreoz.guice; - -import com.coreoz.plume.services.time.TimeProvider; - -import javax.inject.Singleton; -import java.time.Clock; - -@Singleton -public class TimeProviderForTest implements TimeProvider { - private Clock clock; - - public TimeProviderForTest() { - this.clock = Clock.systemDefaultZone(); - } - - @Override - public Clock clock() { - return clock; - } - - public void changeClock(Clock clock) { - this.clock = clock; - } - - public void withClock(Clock clock, Runnable toExecuteWithClock) { - Clock oldClock = this.clock; - changeClock(clock); - toExecuteWithClock.run(); - changeClock(oldClock); - } -} diff --git a/src/test/java/com/coreoz/integration/SampleIntegrationTest.java b/src/test/java/com/coreoz/integration/SampleIntegrationTest.java index 5580232..47878ee 100644 --- a/src/test/java/com/coreoz/integration/SampleIntegrationTest.java +++ b/src/test/java/com/coreoz/integration/SampleIntegrationTest.java @@ -10,6 +10,21 @@ import javax.inject.Inject; +/** + * An integration test sample. + * + * This tests differs from an unit tests, cf {@link SampleTest}, because: + * - It will initialize and rely on the dependency injection, see {@link TestModule} for tests specific overrides + * - Other services can be referenced for this tests + * - These other services can be altered for tests, see {@link TimeProviderForTest} for an example + * - If a database is used in the project, an H2 in memory database will be available to run queries and verify that data is correctly being inserted/updated in the database + * - The H2 in memory database will be created by playing Flyway initialization scripts: these scripts must be correctly setup + * + * Integration tests are a great tool to test the whole chain of services with one automated test. + * Although, to test intensively a function, a unit test is preferred, see {@link TimeProviderForTest} for an example. + * + * Once there are other integration tests in the project, this sample should be deleted. + */ @RunWith(GuiceTestRunner.class) @GuiceModules(TestModule.class) public class SampleIntegrationTest {