diff --git a/.github/workflows/prd-build-deploy.yml b/.github/workflows/prd-build-deploy.yml index b283181..129e023 100644 --- a/.github/workflows/prd-build-deploy.yml +++ b/.github/workflows/prd-build-deploy.yml @@ -14,9 +14,24 @@ jobs: run: | docker-compose -f docker/prd/prd-run-database-job.yml up -d --build env: - POSTGRES_PASSWORD: ${{ secrets.EXAMPLE_DB_PRD_PASSWORD }}" + DB_PRD_USER: ${{ secrets.DB_PRD_USER }} + DB_PRD_PASSWORD: ${{ secrets.DB_PRD_PASSWORD }} - name: Build and deploy the Docker image run: | docker-compose -f docker/prd/prd-build-deploy-job.yml up -d --build env: - POSTGRES_PASSWORD: ${{ secrets.EXAMPLE_DB_PRD_PASSWORD }}" \ No newline at end of file + DB_PRD_ADDRESS: ${{ secrets.DB_PRD_ADDRESS }} + DB_PRD_USER: ${{ secrets.DB_PRD_USER }} + DB_PRD_PASSWORD: ${{ secrets.DB_PRD_PASSWORD }} + EUREKA_PRD: ${{ secrets.EUREKA_PRD }} + PRD_KEYCLOAK_ISSUER_ID: ${{ secrets.PRD_KEYCLOAK_ISSUER_ID }} + PRD_KEYCLOAK_CLIENT_NAME: ${{ secrets.PRD_KEYCLOAK_CLIENT_NAME }} + PRD_KEYCLOAK_CLIENT_ID: ${{ secrets.PRD_KEYCLOAK_CLIENT_ID }} + PRD_KEYCLOAK_CLIENT_SECRET: ${{ secrets.PRD_KEYCLOAK_CLIENT_SECRET }} + PRD_KEYCLOAK_JWK_SET_URI: ${{ secrets.PRD_KEYCLOAK_JWK_SET_URI }} + PRD_KEYCLOAK_URI: ${{ secrets.PRD_KEYCLOAK_URI }} + PRD_KEYCLOAK_REALM_NAME: ${{ secrets.PRD_KEYCLOAK_REALM_NAME }} + PRD_MANAGEMENT_KEYCLOAK_CLIENT_ID: ${{ secrets.PRD_MANAGEMENT_KEYCLOAK_CLIENT_ID }} + PRD_MANAGEMENT_KEYCLOAK_CLIENT_SECRET: ${{ secrets.PRD_MANAGEMENT_KEYCLOAK_CLIENT_SECRET }} + PRD_OSU_API_CLIENT_ID: ${{ secrets.PRD_OSU_API_CLIENT_ID }} + PRD_OSU_API_CLIENT_SECRET: ${{ secrets.PRD_OSU_API_CLIENT_SECRET }} \ No newline at end of file diff --git a/.github/workflows/stg-build-deploy.yml b/.github/workflows/stg-build-deploy.yml index ce5643a..05d859d 100644 --- a/.github/workflows/stg-build-deploy.yml +++ b/.github/workflows/stg-build-deploy.yml @@ -17,9 +17,23 @@ jobs: run: | docker-compose -f docker/stg/stg-run-database-job.yml up -d --build env: - POSTGRES_PASSWORD: ${{ secrets.EXAMPLE_DB_STG_PASSWORD }} + DB_STG_USER: ${{ secrets.DB_STG_USER }} + DB_STG_PASSWORD: ${{ secrets.DB_STG_PASSWORD }} - name: Build and deploy the Docker image run: | docker-compose -f docker/stg/stg-build-deploy-job.yml up -d --build env: - POSTGRES_PASSWORD: ${{ secrets.EXAMPLE_DB_STG_PASSWORD }} \ No newline at end of file + DB_STG_ADDRESS: ${{ secrets.DB_STG_ADDRESS }} + DB_STG_USER: ${{ secrets.DB_STG_USER }} + DB_STG_PASSWORD: ${{ secrets.DB_STG_PASSWORD }} + EUREKA_STG: ${{ secrets.EUREKA_STG }} + STG_KEYCLOAK_NAME_ISSUER: ${{ secrets.STG_KEYCLOAK_NAME_ISSUER }} + STG_KEYCLOAK_CLIENT_NAME: ${{ secrets.STG_KEYCLOAK_CLIENT_NAME }} + STG_KEYCLOAK_CLIENT_ID: ${{ secrets.STG_KEYCLOAK_CLIENT_ID }} + STG_KEYCLOAK_CLIENT_SECRET: ${{ secrets.STG_KEYCLOAK_CLIENT_SECRET }} + STG_KEYCLOAK_URI: ${{ secrets.STG_KEYCLOAK_URI }} + STG_KEYCLOAK_REALM_NAME: ${{ secrets.STG_KEYCLOAK_REALM_NAME }} + STG_MANAGEMENT_KEYCLOAK_CLIENT_ID: ${{ secrets.STG_MANAGEMENT_KEYCLOAK_CLIENT_ID }} + STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET: ${{ secrets.STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET }} + STG_OSU_API_CLIENT_ID: ${{ secrets.STG_OSU_API_CLIENT_ID }} + STG_OSU_API_CLIENT_SECRET: ${{ secrets.STG_OSU_API_CLIENT_SECRET }} \ No newline at end of file diff --git a/.github/workflows/stg-check-build.yml b/.github/workflows/stg-check-build.yml index 6f9b922..e953d06 100644 --- a/.github/workflows/stg-check-build.yml +++ b/.github/workflows/stg-check-build.yml @@ -5,6 +5,8 @@ on: branches: [ "stage" ] pull_request: branches: [ "stage" ] + workflow_dispatch: + branches: [ "stage" ] jobs: build: @@ -17,4 +19,32 @@ jobs: java-version: '17' distribution: 'temurin' - name: Maven Verify - run: mvn --batch-mode --update-snapshots verify \ No newline at end of file + run: mvn --batch-mode --update-snapshots verify -Pstg + - name: Upload coverage + uses: actions/upload-artifact@v3 + with: + name: test-case reports + path: target/site/jacoco + codacy-coverage-reporter: + runs-on: ubuntu-latest + name: codacy-coverage-reporter + needs: [ build ] + steps: + - uses: actions/checkout@v3 + - name: download reports + uses: actions/download-artifact@v3 + with: + name: test-case reports + path: target/site/jacoco + - name: Display structure of downloaded files + run: ls -R + - name: Run codacy-coverage-reporter + uses: codacy/codacy-coverage-reporter-action@v1 + env: + CODACY_API_TOKEN: ${{ secrets.CODACY_API_TOKEN }} + CODACY_ORGANIZATION_PROVIDER: gh + CODACY_USERNAME: AimCup + CODACY_PROJECT_NAME: ${{ github.event.repository.name }} + with: + api-token: $CODACY_API_TOKEN + coverage-reports: ${{ github.workspace }}/target/site/jacoco/jacoco.xml \ No newline at end of file diff --git a/docker/dev/dev-run-database-job.yml b/docker/dev/dev-run-database-job.yml new file mode 100644 index 0000000..c3e7ed8 --- /dev/null +++ b/docker/dev/dev-run-database-job.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + db-user-microservice: + image: postgres:15.1 + restart: always + environment: + POSTGRES_USER: testUser + POSTGRES_PASSWORD: testPassword + POSTGRES_DB: user + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U testUser user" ] + interval: 5s + timeout: 5s + retries: 5 + ports: + - "5432:5432" \ No newline at end of file diff --git a/docker/prd/prd-build-deploy-job.yml b/docker/prd/prd-build-deploy-job.yml index f00a690..bd8e4bb 100644 --- a/docker/prd/prd-build-deploy-job.yml +++ b/docker/prd/prd-build-deploy-job.yml @@ -1,14 +1,28 @@ version: '3.8' services: - example-microservice: + user-microservice: build: context: ../.. dockerfile: docker/prd/prd-build-deploy.Dockerfile ports: - - "8101:8101" + - "8501:8501" environment: - - EXAMPLE_DB_PRD_PASSWORD=${POSTGRES_PASSWORD} + - DB_PRD_ADDRESS=${DB_PRD_ADDRESS} + - DB_PRD_USER=${DB_PRD_USER} + - DB_PRD_PASSWORD=${DB_PRD_PASSWORD} + - EUREKA_PRD=${EUREKA_PRD} + - PRD_KEYCLOAK_ISSUER_ID=${PRD_KEYCLOAK_ISSUER_ID} + - PRD_KEYCLOAK_CLIENT_NAME=${PRD_KEYCLOAK_CLIENT_NAME} + - PRD_KEYCLOAK_CLIENT_ID=${PRD_KEYCLOAK_CLIENT_ID} + - PRD_KEYCLOAK_CLIENT_SECRET=${PRD_KEYCLOAK_CLIENT_SECRET} + - PRD_KEYCLOAK_JWK_SET_URI=${PRD_KEYCLOAK_JWK_SET_URI} + - PRD_KEYCLOAK_URI = ${PRD_KEYCLOAK_URI} + - PRD_KEYCLOAK_REALM_NAME = ${PRD_KEYCLOAK_REALM_NAME} + - PRD_MANAGEMENT_KEYCLOAK_CLIENT_ID = ${PRD_MANAGEMENT_KEYCLOAK_CLIENT_ID} + - PRD_MANAGEMENT_KEYCLOAK_CLIENT_SECRET = ${PRD_MANAGEMENT_KEYCLOAK_CLIENT_SECRET} + - PRD_OSU_API_CLIENT_SECRET = ${PRD_OSU_API_CLIENT_SECRET} + - PRD_OSU_API_CLIENT_ID = ${PRD_OSU_API_CLIENT_ID} networks: default: diff --git a/docker/prd/prd-build-deploy.Dockerfile b/docker/prd/prd-build-deploy.Dockerfile index 3bae33b..002b3be 100644 --- a/docker/prd/prd-build-deploy.Dockerfile +++ b/docker/prd/prd-build-deploy.Dockerfile @@ -5,5 +5,5 @@ RUN mvn -f /acservice/pom.xml clean package -P prd FROM arm32v7/eclipse-temurin:17 COPY --from=build /acservice/target/*.jar app.jar -EXPOSE 8101 +EXPOSE 8501 ENTRYPOINT ["java","-Dspring.profiles.active=prd","-jar","/app.jar"] diff --git a/docker/prd/prd-run-database-job.yml b/docker/prd/prd-run-database-job.yml index 67a6879..6c096ad 100644 --- a/docker/prd/prd-run-database-job.yml +++ b/docker/prd/prd-run-database-job.yml @@ -1,20 +1,20 @@ version: '3.8' services: - db-example-microservice: + db-user-microservice: image: postgres:15.1 restart: always environment: - POSTGRES_USER: example-db-username - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: example-db-name + POSTGRES_USER: ${DB_PRD_USER} + POSTGRES_PASSWORD: ${DB_PRD_PASSWORD} + POSTGRES_DB: user healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres" ] + test: [ "CMD-SHELL", "pg_isready -U $$DB_PRD_USER user" ] interval: 5s timeout: 5s retries: 5 ports: - - "5401:5432" + - "5701:5432" networks: default: diff --git a/docker/stg/stg-build-deploy-job.yml b/docker/stg/stg-build-deploy-job.yml index a94d4d0..f6f32d8 100644 --- a/docker/stg/stg-build-deploy-job.yml +++ b/docker/stg/stg-build-deploy-job.yml @@ -1,15 +1,28 @@ version: '3.8' services: - example-microservice: + user-microservice: build: context: ../.. dockerfile: docker/stg/stg-build-deploy.Dockerfile ports: - - "8201:8201" + - "8502:8502" environment: - - EXAMPLE_DB_STG_PASSWORD=${POSTGRES_PASSWORD} - + - DB_STG_ADDRESS=${DB_STG_ADDRESS} + - DB_STG_USER=${DB_STG_USER} + - DB_STG_PASSWORD=${DB_STG_PASSWORD} + - EUREKA_STG=${EUREKA_STG} + - STG_KEYCLOAK_NAME_ISSUER=${STG_KEYCLOAK_NAME_ISSUER} + - STG_KEYCLOAK_CLIENT_NAME=${STG_KEYCLOAK_CLIENT_NAME} + - STG_KEYCLOAK_CLIENT_ID=${STG_KEYCLOAK_CLIENT_ID} + - STG_KEYCLOAK_CLIENT_SECRET=${STG_KEYCLOAK_CLIENT_SECRET} + - STG_KEYCLOAK_JWK_SET_URI=${STG_KEYCLOAK_JWK_SET_URI} + - STG_KEYCLOAK_URI = ${STG_KEYCLOAK_URI} + - STG_KEYCLOAK_REALM_NAME = ${STG_KEYCLOAK_REALM_NAME} + - STG_MANAGEMENT_KEYCLOAK_CLIENT_ID = ${STG_MANAGEMENT_KEYCLOAK_CLIENT_ID} + - STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET = ${STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET} + - STG_OSU_API_CLIENT_SECRET = ${STG_OSU_API_CLIENT_SECRET} + - STG_OSU_API_CLIENT_ID = ${STG_OSU_API_CLIENT_ID} networks: default: diff --git a/docker/stg/stg-build-deploy.Dockerfile b/docker/stg/stg-build-deploy.Dockerfile index d8f1e5b..d94c606 100644 --- a/docker/stg/stg-build-deploy.Dockerfile +++ b/docker/stg/stg-build-deploy.Dockerfile @@ -1,9 +1,9 @@ -FROM arm32v7/maven:3.9.3-eclipse-temurin-17 AS build +FROM maven:3.9.3-eclipse-temurin-17-focal AS build COPY src /acservice/src COPY pom.xml /acservice RUN mvn -f /acservice/pom.xml clean package -P stg -FROM arm32v7/eclipse-temurin:17 +FROM eclipse-temurin:17.0.8_7-jre-focal COPY --from=build /acservice/target/*.jar app.jar -EXPOSE 8201 +EXPOSE 8502 ENTRYPOINT ["java","-Dspring.profiles.active=stg","-jar","/app.jar"] diff --git a/docker/stg/stg-run-database-job.yml b/docker/stg/stg-run-database-job.yml index fbb95be..ac9e413 100644 --- a/docker/stg/stg-run-database-job.yml +++ b/docker/stg/stg-run-database-job.yml @@ -1,20 +1,20 @@ version: '3.8' services: - db-example-microservice: + db-user-microservice: image: postgres:15.1 restart: always environment: - POSTGRES_USER: example-db-username - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_DB: example-db-name + POSTGRES_USER: ${DB_STG_USER} + POSTGRES_PASSWORD: ${DB_STG_PASSWORD} + POSTGRES_DB: user healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres" ] + test: [ "CMD-SHELL", "pg_isready -U $$DB_STG_USER user" ] interval: 5s timeout: 5s retries: 5 ports: - - "5501:5432" + - "5702:5432" networks: default: diff --git a/pom.xml b/pom.xml index c8b5254..6885f20 100644 --- a/pom.xml +++ b/pom.xml @@ -9,9 +9,9 @@ xyz.aimcup - template-repository - 1.0.0 - template-repository + user-microservice + 1.0.1-SNAPSHOT + user-microservice 17 2022.0.3 @@ -28,12 +28,32 @@ spring-cloud-starter-netflix-eureka-client - org.springframework.security - spring-security-oauth2-resource-server + org.springframework.boot + spring-boot-starter-data-jpa + + org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.3.1 @@ -56,8 +76,31 @@ org.springdoc - springdoc-openapi-ui - 1.7.0 + springdoc-openapi-starter-webmvc-ui + 2.2.0 + + + + + org.keycloak + keycloak-admin-client + 22.0.5 + + + org.jboss + jandex + + + + + org.keycloak + keycloak-adapter-core + 22.0.5 + + + org.keycloak + keycloak-common + 22.0.5 @@ -71,7 +114,10 @@ mapstruct 1.5.5.Final - + + org.springframework.cloud + spring-cloud-starter-openfeign + @@ -144,19 +190,46 @@ - org.apache.maven.plugins - maven-failsafe-plugin + org.jacoco + jacoco-maven-plugin + 0.8.10 - -Dspring.profiles.active=test - - **/*IT.java - + + xyz/aimcup/generated/**/* + xyz/aimcup/**/configuration/**/* + xyz/aimcup/**/data/**/* + xyz/aimcup/**/mapper/**/* + + prepare-jacoco-agent-test - integration-test - verify + prepare-agent + + + + generate-code-coverage-report-mvn-test + test + + report + + + + prepare-jacoco-agent-verify + pre-integration-test + + prepare-agent + + + failsafe.jacoco.args + + + + generate-code-coverage-report-mvn-verify + post-integration-test + + report @@ -179,7 +252,10 @@ xyz.aimcup.generated.model false - @lombok.NoArgsConstructor @lombok.Builder @lombok.AllArgsConstructor + true + @lombok.NoArgsConstructor @lombok.Builder + @lombok.AllArgsConstructor + true true false @@ -211,5 +287,4 @@ - diff --git a/src/main/java/xyz/aimcup/example/ExampleApplication.java b/src/main/java/xyz/aimcup/auth/AuthApplication.java similarity index 59% rename from src/main/java/xyz/aimcup/example/ExampleApplication.java rename to src/main/java/xyz/aimcup/auth/AuthApplication.java index 7f90d46..4eeba6e 100644 --- a/src/main/java/xyz/aimcup/example/ExampleApplication.java +++ b/src/main/java/xyz/aimcup/auth/AuthApplication.java @@ -1,13 +1,15 @@ -package xyz.aimcup.example; +package xyz.aimcup.auth; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; +import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient -public class ExampleApplication { +@EnableFeignClients +public class AuthApplication { public static void main(String[] args) { - SpringApplication.run(ExampleApplication.class, args); + SpringApplication.run(AuthApplication.class, args); } } diff --git a/src/main/java/xyz/aimcup/auth/configuration/RestTemplateConfiguration.java b/src/main/java/xyz/aimcup/auth/configuration/RestTemplateConfiguration.java new file mode 100644 index 0000000..399c514 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/configuration/RestTemplateConfiguration.java @@ -0,0 +1,14 @@ +package xyz.aimcup.auth.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfiguration { + + @Bean + public RestTemplate httpClient() { + return new RestTemplate(); + } +} diff --git a/src/main/java/xyz/aimcup/auth/configuration/SecurityConfiguration.java b/src/main/java/xyz/aimcup/auth/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..3676df7 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/configuration/SecurityConfiguration.java @@ -0,0 +1,28 @@ +package xyz.aimcup.auth.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + + @Bean + public SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .oauth2Client(Customizer.withDefaults()) + .oauth2Login(Customizer.withDefaults()); + httpSecurity + .sessionManagement(Customizer.withDefaults()); + httpSecurity.csrf(AbstractHttpConfigurer::disable); + httpSecurity.oauth2ResourceServer(auth -> auth.jwt(Customizer.withDefaults())); + return httpSecurity.build(); + } +} diff --git a/src/main/java/xyz/aimcup/auth/controller/UserController.java b/src/main/java/xyz/aimcup/auth/controller/UserController.java new file mode 100644 index 0000000..b56035e --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/controller/UserController.java @@ -0,0 +1,33 @@ +package xyz.aimcup.auth.controller; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.RestController; +import xyz.aimcup.auth.data.entity.User; +import xyz.aimcup.auth.mapper.user.UserMapper; +import xyz.aimcup.auth.service.UserService; +import xyz.aimcup.generated.UserApi; +import xyz.aimcup.generated.model.UserResponseDTO; + +@RestController +@RequiredArgsConstructor +public class UserController implements UserApi { + private final UserService userService; + private final UserMapper userMapper; + + @Secured("OIDC_USER") + @Override + public ResponseEntity getUserByOsuId(String osuId) { + User user = userService.getUserByOsuId(osuId); + UserResponseDTO userResponseDTO = userMapper.userToUserResponseDto(user); + return ResponseEntity.ok(userResponseDTO); + } + + // TODO: AC-61 + @Override + public ResponseEntity me() { + return null; + } + +} diff --git a/src/main/java/xyz/aimcup/auth/data/entity/User.java b/src/main/java/xyz/aimcup/auth/data/entity/User.java new file mode 100644 index 0000000..390850c --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/data/entity/User.java @@ -0,0 +1,34 @@ +package xyz.aimcup.auth.data.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Entity +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID id; + + @Column(name = "username", unique = true) + private String username; + + @Column(name = "osu_id", unique = true) + private String osuId; + + @Column(name = "is_restricted") + private Boolean isRestricted; +} diff --git a/src/main/java/xyz/aimcup/auth/data/repository/UserRepository.java b/src/main/java/xyz/aimcup/auth/data/repository/UserRepository.java new file mode 100644 index 0000000..cdcd0f9 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/data/repository/UserRepository.java @@ -0,0 +1,12 @@ +package xyz.aimcup.auth.data.repository; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import xyz.aimcup.auth.data.entity.User; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/xyz/aimcup/auth/feign/FeignExceptionErrorDecoder.java b/src/main/java/xyz/aimcup/auth/feign/FeignExceptionErrorDecoder.java new file mode 100644 index 0000000..82e2a65 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/FeignExceptionErrorDecoder.java @@ -0,0 +1,22 @@ +package xyz.aimcup.auth.feign; + +import feign.Response; +import feign.codec.ErrorDecoder; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; + +public class FeignExceptionErrorDecoder implements ErrorDecoder { + private final ErrorDecoder errorDecoder = new Default(); + + @Override + public Exception decode(String s, Response response) { + switch (response.status()) { + case 400: + return new ResponseStatusException(HttpStatus.BAD_REQUEST, "Bad request"); + case 404: + return new ResponseStatusException(HttpStatus.NOT_FOUND, "User with provided id not found"); + default: + return errorDecoder.decode(s, response); + } + } +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClient.java b/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClient.java new file mode 100644 index 0000000..d5b9b50 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClient.java @@ -0,0 +1,15 @@ +package xyz.aimcup.auth.feign.osu; + +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; + +@FeignClient(name = "osu-api", + url = "https://osu.ppy.sh/api/v2/", + configuration = FeignOsuClientConfiguration.class) +public interface FeignOsuClient { + + @GetMapping("/users/{id}/osu") + OsuUserExtended getUserById(@PathVariable String id); +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClientConfiguration.java b/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClientConfiguration.java new file mode 100644 index 0000000..5036210 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/FeignOsuClientConfiguration.java @@ -0,0 +1,52 @@ +package xyz.aimcup.auth.feign.osu; + +import feign.RequestInterceptor; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Scope; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; +import xyz.aimcup.auth.feign.osu.model.OsuToken; +import xyz.aimcup.auth.properties.OsuApiProperties; +import xyz.aimcup.auth.properties.OsuProperties; + +@Log4j2 +@RequiredArgsConstructor +class FeignOsuClientConfiguration { + private final RestTemplate restTemplate; + private final OsuProperties osuProperties; + + @Bean + @Scope("prototype") + public RequestInterceptor osuAccessTokenInterceptor() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Accept", MediaType.APPLICATION_FORM_URLENCODED_VALUE); + + OsuApiProperties osuApiProperties = osuProperties.getApi(); + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("client_id", osuApiProperties.getClientId()); + map.add("client_secret", osuApiProperties.getClientSecret()); + map.add("grant_type", "client_credentials"); + map.add("scope", "public"); + HttpEntity> entity = new HttpEntity<>(map, headers); + ResponseEntity response = + restTemplate.exchange("https://osu.ppy.sh/oauth/token", + HttpMethod.POST, + entity, + OsuToken.class); + if (response.getBody() == null) { + log.error("Failed to get access token from osu api"); + throw new RuntimeException("Failed to get access token from osu api"); + } + String accessToken = response.getBody().getAccessToken(); + return requestTemplate -> requestTemplate.header("Authorization", "Bearer " + accessToken); + } +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/OsuClient.java b/src/main/java/xyz/aimcup/auth/feign/osu/OsuClient.java new file mode 100644 index 0000000..69dda45 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/OsuClient.java @@ -0,0 +1,8 @@ +package xyz.aimcup.auth.feign.osu; + +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; + +public interface OsuClient { + OsuUserExtended getUserById(String id); + +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImpl.java b/src/main/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImpl.java new file mode 100644 index 0000000..e61ea96 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImpl.java @@ -0,0 +1,30 @@ +package xyz.aimcup.auth.feign.osu.impl; + +import feign.FeignException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import xyz.aimcup.auth.feign.osu.FeignOsuClient; +import xyz.aimcup.auth.feign.osu.OsuClient; +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; + +@Service +@RequiredArgsConstructor +public class OsuClientImpl implements OsuClient { + private final FeignOsuClient osuClient; + + @Override + public OsuUserExtended getUserById(String id) { + try { + return osuClient.getUserById(id); + } catch (FeignException.NotFound e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "User with id " + id + " not found"); + } catch (FeignException.BadRequest e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Bad request"); + } catch (Exception e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "External API call failed"); + } + } + +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuAccessToken.java b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuAccessToken.java new file mode 100644 index 0000000..64707c7 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuAccessToken.java @@ -0,0 +1,17 @@ +package xyz.aimcup.auth.feign.osu.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@AllArgsConstructor +@Data +public class OsuAccessToken { + + private String clientId; + + private String clientSecret; + + private String grantType; + + private String scope; +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuToken.java b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuToken.java new file mode 100644 index 0000000..c70824a --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuToken.java @@ -0,0 +1,20 @@ +package xyz.aimcup.auth.feign.osu.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class OsuToken { + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("expires_in") + private int expiresIn; + + @JsonProperty("refresh_token") + private String refreshToken; +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUser.java b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUser.java new file mode 100644 index 0000000..604eac3 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUser.java @@ -0,0 +1,15 @@ +package xyz.aimcup.auth.feign.osu.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OsuUser { + private Long id; + private String username; + @JsonProperty("avatar_url") + private String avatarUrl; + +} diff --git a/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUserExtended.java b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUserExtended.java new file mode 100644 index 0000000..31e9eba --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/feign/osu/model/OsuUserExtended.java @@ -0,0 +1,31 @@ +package xyz.aimcup.auth.feign.osu.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class OsuUserExtended extends OsuUser { + + @JsonProperty("cover_url") + private String coverUrl; + + @JsonProperty("discord") + private String discord; + + @JsonProperty("has_supported") + private Boolean hasSupported; + + @JsonProperty("interests") + private String interests; + + @JsonProperty("join_date") + private String joinDate; + + @JsonProperty("location") + private String location; + + @JsonProperty("title") + private String title; +} diff --git a/src/main/java/xyz/aimcup/auth/mapper/user/UserMapper.java b/src/main/java/xyz/aimcup/auth/mapper/user/UserMapper.java new file mode 100644 index 0000000..30bfe56 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/mapper/user/UserMapper.java @@ -0,0 +1,18 @@ +package xyz.aimcup.auth.mapper.user; + +import org.keycloak.representations.idm.UserRepresentation; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import xyz.aimcup.auth.data.entity.User; +import xyz.aimcup.generated.model.UserResponseDTO; + +@Mapper(componentModel = "spring") +public interface UserMapper { + UserResponseDTO userToUserResponseDto(User user); + + + @Mapping(target = "osuId", source = "username") + @Mapping(target = "username", source = "username") + @Mapping(target = "isRestricted", source = "enabled") + User userRepresentationToUser(UserRepresentation userRepresentation); +} diff --git a/src/main/java/xyz/aimcup/auth/properties/AimCupProperties.java b/src/main/java/xyz/aimcup/auth/properties/AimCupProperties.java new file mode 100644 index 0000000..6dd35a1 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/properties/AimCupProperties.java @@ -0,0 +1,12 @@ +package xyz.aimcup.auth.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "aimcup") +@Data +public class AimCupProperties { + private KeycloakProperties keycloak; +} diff --git a/src/main/java/xyz/aimcup/auth/properties/KeycloakProperties.java b/src/main/java/xyz/aimcup/auth/properties/KeycloakProperties.java new file mode 100644 index 0000000..b6afd06 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/properties/KeycloakProperties.java @@ -0,0 +1,15 @@ +package xyz.aimcup.auth.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "keycloak") +@Data +public class KeycloakProperties { + private String serverUrl; + private String realm; + private String clientId; + private String clientSecret; +} diff --git a/src/main/java/xyz/aimcup/auth/properties/OsuApiProperties.java b/src/main/java/xyz/aimcup/auth/properties/OsuApiProperties.java new file mode 100644 index 0000000..c57aa63 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/properties/OsuApiProperties.java @@ -0,0 +1,13 @@ +package xyz.aimcup.auth.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "api") +@Data +public class OsuApiProperties { + private String clientId; + private String clientSecret; +} diff --git a/src/main/java/xyz/aimcup/auth/properties/OsuProperties.java b/src/main/java/xyz/aimcup/auth/properties/OsuProperties.java new file mode 100644 index 0000000..c62f4c1 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/properties/OsuProperties.java @@ -0,0 +1,12 @@ +package xyz.aimcup.auth.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "osu") +@Data +public class OsuProperties { + private OsuApiProperties api; +} diff --git a/src/main/java/xyz/aimcup/auth/service/KeycloakService.java b/src/main/java/xyz/aimcup/auth/service/KeycloakService.java new file mode 100644 index 0000000..f489e86 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/service/KeycloakService.java @@ -0,0 +1,16 @@ +package xyz.aimcup.auth.service; + +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.UserRepresentation; +import xyz.aimcup.auth.feign.osu.model.OsuUser; + +import java.util.UUID; + +public interface KeycloakService { + Keycloak getKeycloak(); + + UserRepresentation createUser(OsuUser osuUser); + + UUID createUser(UserRepresentation userRepresentation); + +} diff --git a/src/main/java/xyz/aimcup/auth/service/UserService.java b/src/main/java/xyz/aimcup/auth/service/UserService.java new file mode 100644 index 0000000..420ce68 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/service/UserService.java @@ -0,0 +1,10 @@ +package xyz.aimcup.auth.service; + + +import xyz.aimcup.auth.data.entity.User; + +public interface UserService { + + User getUserByOsuId(String osuId); + +} diff --git a/src/main/java/xyz/aimcup/auth/service/impl/KeycloakServiceImpl.java b/src/main/java/xyz/aimcup/auth/service/impl/KeycloakServiceImpl.java new file mode 100644 index 0000000..4a7f982 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/service/impl/KeycloakServiceImpl.java @@ -0,0 +1,61 @@ +package xyz.aimcup.auth.service.impl; + +import jakarta.ws.rs.core.Response; +import lombok.RequiredArgsConstructor; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.KeycloakBuilder; +import org.keycloak.representations.idm.UserRepresentation; +import org.springframework.stereotype.Service; +import xyz.aimcup.auth.feign.osu.model.OsuUser; +import xyz.aimcup.auth.properties.AimCupProperties; +import xyz.aimcup.auth.properties.KeycloakProperties; +import xyz.aimcup.auth.service.KeycloakService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class KeycloakServiceImpl implements KeycloakService { + private final AimCupProperties aimCupProperties; + + public Keycloak getKeycloak() { + KeycloakProperties keycloakProperties = aimCupProperties.getKeycloak(); + return KeycloakBuilder.builder() + .serverUrl(keycloakProperties.getServerUrl()) + .grantType(OAuth2Constants.CLIENT_CREDENTIALS) + .realm(keycloakProperties.getRealm()) + .clientId(keycloakProperties.getClientId()) + .clientSecret(keycloakProperties.getClientSecret()) + .build(); + } + + @Override + public UserRepresentation createUser(OsuUser osuUser) { + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setUsername(osuUser.getId().toString()); + userRepresentation.setEnabled(true); + Map> attributes = new HashMap<>(); + attributes.put("username", List.of(osuUser.getUsername())); + userRepresentation.setAttributes(attributes); + UUID userId = this.createUser(userRepresentation); + userRepresentation.setId(userId.toString()); + return userRepresentation; + } + + @Override + public UUID createUser(UserRepresentation userRepresentation) { + KeycloakProperties keycloakProperties = aimCupProperties.getKeycloak(); + try (Response response = + this.getKeycloak() + .realm(keycloakProperties.getRealm()) + .users() + .create(userRepresentation)) { + return UUID.fromString(response.getLocation().getPath().replaceAll(".*/([^/]+)$", "$1")); + } + } + +} diff --git a/src/main/java/xyz/aimcup/auth/service/impl/UserServiceImpl.java b/src/main/java/xyz/aimcup/auth/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..13ab6a8 --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/service/impl/UserServiceImpl.java @@ -0,0 +1,42 @@ +package xyz.aimcup.auth.service.impl; + +import lombok.RequiredArgsConstructor; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.UserRepresentation; +import org.springframework.stereotype.Service; +import xyz.aimcup.auth.data.entity.User; +import xyz.aimcup.auth.feign.osu.OsuClient; +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; +import xyz.aimcup.auth.mapper.user.UserMapper; +import xyz.aimcup.auth.properties.AimCupProperties; +import xyz.aimcup.auth.service.KeycloakService; +import xyz.aimcup.auth.service.UserService; + + +@Service +@RequiredArgsConstructor +public class UserServiceImpl implements UserService { + private final KeycloakService keycloakService; + private final OsuClient osuClient; + private final UserMapper userMapper; + private final AimCupProperties aimCupProperties; + + @Override + public User getUserByOsuId(String osuId) { + Keycloak keycloak = keycloakService.getKeycloak(); + return keycloak.realm(aimCupProperties.getKeycloak().getRealm()) + .users() + .searchByUsername(osuId, true) + .stream() + .filter(u -> u.getUsername().equals(osuId)) + .findFirst() + .map(userMapper::userRepresentationToUser) + .orElseGet(() -> createUserByOsuId(osuId)); + } + + private User createUserByOsuId(String osuId) { + OsuUserExtended osuUser = osuClient.getUserById(osuId); + UserRepresentation userRepresentation = keycloakService.createUser(osuUser); + return userMapper.userRepresentationToUser(userRepresentation); + } +} diff --git a/src/main/java/xyz/aimcup/auth/util/CookieUtils.java b/src/main/java/xyz/aimcup/auth/util/CookieUtils.java new file mode 100644 index 0000000..dec236e --- /dev/null +++ b/src/main/java/xyz/aimcup/auth/util/CookieUtils.java @@ -0,0 +1,60 @@ +package xyz.aimcup.auth.util; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.util.SerializationUtils; + +import java.util.Base64; +import java.util.Optional; + +public class CookieUtils { + + public static Optional getCookie(HttpServletRequest request, String name) { + Cookie[] cookies = request.getCookies(); + + if (cookies != null && cookies.length > 0) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals(name)) { + return Optional.of(cookie); + } + } + } + + return Optional.empty(); + } + + public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setHttpOnly(true); + cookie.setMaxAge(maxAge); + response.addCookie(cookie); + } + + public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { + Cookie[] cookies = request.getCookies(); + if (cookies != null && cookies.length > 0) { + for (Cookie cookie: cookies) { + if (cookie.getName().equals(name)) { + cookie.setValue(""); + cookie.setPath("/"); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + } + } + + public static String serialize(Object object) { + return Base64.getUrlEncoder() + .encodeToString(SerializationUtils.serialize(object)); + } + + public static T deserialize(Cookie cookie, Class cls) { + return cls.cast(SerializationUtils.deserialize( + Base64.getUrlDecoder().decode(cookie.getValue()))); + } + + +} diff --git a/src/main/java/xyz/aimcup/example/controller/ExampleController.java b/src/main/java/xyz/aimcup/example/controller/ExampleController.java deleted file mode 100644 index fa5bf8f..0000000 --- a/src/main/java/xyz/aimcup/example/controller/ExampleController.java +++ /dev/null @@ -1,36 +0,0 @@ -package xyz.aimcup.example.controller; - -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.RestController; -import xyz.aimcup.generated.ExamplesApi; -import xyz.aimcup.generated.model.ExampleDataRequest; -import xyz.aimcup.generated.model.ExampleDataResponse; -import xyz.aimcup.example.data.entity.Example; -import xyz.aimcup.example.data.repository.ExampleRepostiory; -import xyz.aimcup.example.mapper.example.ExampleMapper; - -import java.util.List; - -@RestController -@RequiredArgsConstructor -public class ExampleController implements ExamplesApi { - - private final ExampleRepostiory exampleRepostiory; - private final ExampleMapper exampleMapper; - - @Override - public ResponseEntity addNewExamples(ExampleDataRequest exampleDataRequest) { - exampleRepostiory.save(Example.builder() - .data(exampleDataRequest.getData()) - .build()); - return ResponseEntity.ok("Example added"); - } - - @Override - public ResponseEntity> getExamples() { - List examples = exampleRepostiory.findAll(); - List exampleDataResponses = exampleMapper.examplesToExampleResponses(examples); - return ResponseEntity.ok(exampleDataResponses); - } -} diff --git a/src/main/java/xyz/aimcup/example/data/entity/Example.java b/src/main/java/xyz/aimcup/example/data/entity/Example.java deleted file mode 100644 index 1221d81..0000000 --- a/src/main/java/xyz/aimcup/example/data/entity/Example.java +++ /dev/null @@ -1,50 +0,0 @@ -package xyz.aimcup.example.data.entity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -import java.util.Objects; -import java.util.UUID; - -@Getter -@Setter -@Entity -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class Example { - - @Id - @Column(name = "id") - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "data") - private String data; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Example example = (Example) o; - - if (!Objects.equals(id, example.id)) return false; - return Objects.equals(data, example.data); - } - - @Override - public int hashCode() { - int result = id != null ? id.hashCode() : 0; - result = 31 * result + (data != null ? data.hashCode() : 0); - return result; - } -} diff --git a/src/main/java/xyz/aimcup/example/data/repository/ExampleRepostiory.java b/src/main/java/xyz/aimcup/example/data/repository/ExampleRepostiory.java deleted file mode 100644 index a5679af..0000000 --- a/src/main/java/xyz/aimcup/example/data/repository/ExampleRepostiory.java +++ /dev/null @@ -1,11 +0,0 @@ -package xyz.aimcup.example.data.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; -import xyz.aimcup.example.data.entity.Example; - -import java.util.UUID; - -@Repository -public interface ExampleRepostiory extends JpaRepository { -} diff --git a/src/main/java/xyz/aimcup/example/mapper/example/ExampleMapper.java b/src/main/java/xyz/aimcup/example/mapper/example/ExampleMapper.java deleted file mode 100644 index f57d31f..0000000 --- a/src/main/java/xyz/aimcup/example/mapper/example/ExampleMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package xyz.aimcup.example.mapper.example; - -import org.mapstruct.Mapper; -import xyz.aimcup.generated.model.ExampleDataResponse; -import xyz.aimcup.example.data.entity.Example; - -import java.util.List; - -@Mapper(componentModel = "spring") -public interface ExampleMapper { - ExampleDataResponse exampleToExampleResponse(Example example); - List examplesToExampleResponses(List examples); -} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b7cee23..371bc7b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,13 +3,13 @@ eureka: enabled: false server: servlet: - context-path: /example-path - port: 0 + context-path: /user + port: 8080 spring: application: - name: example-microservice + name: user-microservice datasource: - url: jdbc:postgresql://localhost:5455/example-db-name + url: jdbc:postgresql://localhost:5432/user username: testUser password: testPassword driver-class-name: org.postgresql.Driver @@ -27,3 +27,36 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect + hbm2ddl: + auto: validate + security: + oauth2: + client: + provider: + aimcup: + issuer-uri: ${OSU_KEYCLOAK_ISSUER_ID} + registration: + aimcup: + provider: aimcup + client-name: ${OSU_KEYCLOAK_CLIENT_NAME} + client-id: ${OSU_KEYCLOAK_CLIENT_ID} + client-secret: ${OSU_KEYCLOAK_CLIENT_SECRET} + scope: profile,openid,offline_access + redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' + resourceserver: + jwt: + jwk-set-uri: ${OSU_KEYCLOAK_JWK_SET_URI} + main: + allow-bean-definition-overriding: true + +aimcup: + keycloak: + serverUrl: ${STG_KEYCLOAK_URI} + realm: ${STG_KEYCLOAK_REALM_NAME} + clientId: ${STG_MANAGEMENT_KEYCLOAK_CLIENT_ID} + clientSecret: ${STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET} + +osu: + api: + clientId: ${STG_OSU_API_CLIENT_ID} + clientSecret: ${STG_OSU_API_CLIENT_SECRET} \ No newline at end of file diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml index 9e1cfca..97aed0d 100644 --- a/src/main/resources/application-prd.yml +++ b/src/main/resources/application-prd.yml @@ -1,18 +1,18 @@ eureka: client: service-url: - defaultZone: http://172.18.0.1:8761/eureka + defaultZone: http://${EUREKA_PRD}/eureka server: servlet: - context-path: /example-path - port: 8101 + context-path: /user + port: 8501 spring: application: - name: example-microservice + name: user-microservice datasource: - url: jdbc:postgresql://172.18.0.1:5401/example-db-name - username: example-db-username - password: ${EXAMPLE_DB_PRD_PASSWORD} + url: jdbc:postgresql://${DB_PRD_ADDRESS}/user + username: ${DB_PRD_USER} + password: ${DB_PRD_PASSWORD} driver-class-name: org.postgresql.Driver hikari: connection-timeout: 10000 @@ -28,3 +28,25 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect + security: + oauth2: + client: + provider: + aimcup: + issuer-uri: ${PRD_KEYCLOAK_ISSUER_ID} + registration: + aimcup: + provider: aimcup + client-name: ${PRD_KEYCLOAK_CLIENT_NAME} + client-id: ${PRD_KEYCLOAK_CLIENT_ID} + client-secret: ${PRD_KEYCLOAK_CLIENT_SECRET} + scope: profile,openid,offline_access + redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' + resourceserver: + jwt: + jwk-set-uri: ${PRD_KEYCLOAK_JWK_SET_URI} +springdoc: + api-docs: + enabled: false + swagger-ui: + enabled: false \ No newline at end of file diff --git a/src/main/resources/application-stg.yml b/src/main/resources/application-stg.yml index b39523f..8e84693 100644 --- a/src/main/resources/application-stg.yml +++ b/src/main/resources/application-stg.yml @@ -1,18 +1,19 @@ eureka: client: service-url: - defaultZone: http://172.18.0.1:8762/eureka + defaultZone: http://${EUREKA_STG}/eureka server: servlet: - context-path: /example-path - port: 8201 + context-path: /user + port: 8502 + forward-headers-strategy: native spring: application: - name: example-microservice + name: user-microservice datasource: - url: jdbc:postgresql://172.18.0.1:5501/example-db-name - username: example-db-username - password: ${EXAMPLE_DB_STG_PASSWORD} + url: jdbc:postgresql://${DB_STG_ADDRESS}/user + username: ${DB_STG_USER} + password: ${DB_STG_PASSWORD} driver-class-name: org.postgresql.Driver hikari: connection-timeout: 10000 @@ -28,3 +29,36 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect + hbm2ddl: + auto: validate + security: + oauth2: + client: + provider: + aimcup: + issuer-uri: ${STG_KEYCLOAK_NAME_ISSUER} + registration: + aimcup: + provider: aimcup + client-name: ${STG_KEYCLOAK_CLIENT_NAME} + client-id: ${STG_KEYCLOAK_CLIENT_ID} + client-secret: ${STG_KEYCLOAK_CLIENT_SECRET} + scope: profile,openid,offline_access + redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}' + resourceserver: + jwt: + jwk-set-uri: ${STG_KEYCLOAK_JWK_SET_URI} + main: + allow-bean-definition-overriding: true + +aimcup: + keycloak: + serverUrl: ${STG_KEYCLOAK_URI} + realm: ${STG_KEYCLOAK_REALM_NAME} + clientId: ${STG_MANAGEMENT_KEYCLOAK_CLIENT_ID} + clientSecret: ${STG_MANAGEMENT_KEYCLOAK_CLIENT_SECRET} + +osu: + api: + clientId: ${STG_OSU_API_CLIENT_ID} + clientSecret: ${STG_OSU_API_CLIENT_SECRET} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9d5fb50..c477f03 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1 +1 @@ -spring.profiles.active: @spring.profiles.active@ \ No newline at end of file +spring.profiles.active: @spring.profiles.active@ diff --git a/src/main/resources/db/migration/V1_0__create_user_roles_table.sql b/src/main/resources/db/migration/V1_0__create_user_roles_table.sql new file mode 100644 index 0000000..a55c2e7 --- /dev/null +++ b/src/main/resources/db/migration/V1_0__create_user_roles_table.sql @@ -0,0 +1,12 @@ +CREATE TABLE "user" +( + id UUID PRIMARY KEY, + username VARCHAR NOT NULL, + osu_id VARCHAR NOT NULL, + is_restricted BOOLEAN NOT NULL, + CONSTRAINT user_username_unique UNIQUE (username), + CONSTRAINT user_osuId_unique UNIQUE (osu_id) +); + +CREATE INDEX idx_user_username ON "user" (username); +CREATE INDEX idx_user_osuId ON "user" (osu_id); diff --git a/src/main/resources/db/migration/V1_0__init_migration_to_change.sql b/src/main/resources/db/migration/V1_0__init_migration_to_change.sql deleted file mode 100644 index 63dcfea..0000000 --- a/src/main/resources/db/migration/V1_0__init_migration_to_change.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE example -( - id UUID PRIMARY KEY, - data text NOT NULL -); \ No newline at end of file diff --git a/src/main/resources/openapi/models/example/example-data-request.yaml b/src/main/resources/openapi/models/example/example-data-request.yaml deleted file mode 100644 index e83cd6a..0000000 --- a/src/main/resources/openapi/models/example/example-data-request.yaml +++ /dev/null @@ -1,5 +0,0 @@ -ExampleDataRequest: - type: object - properties: - data: - type: string \ No newline at end of file diff --git a/src/main/resources/openapi/models/example/example-data-response.yaml b/src/main/resources/openapi/models/roles/role-response.yaml similarity index 75% rename from src/main/resources/openapi/models/example/example-data-response.yaml rename to src/main/resources/openapi/models/roles/role-response.yaml index a6122e0..5987693 100644 --- a/src/main/resources/openapi/models/example/example-data-response.yaml +++ b/src/main/resources/openapi/models/roles/role-response.yaml @@ -1,8 +1,8 @@ -ExampleDataResponse: +RoleResponseDTO: type: object properties: id: type: string format: uuid - data: + name: type: string diff --git a/src/main/resources/openapi/models/security/user-principal.yaml b/src/main/resources/openapi/models/security/user-principal.yaml new file mode 100644 index 0000000..e7db3ca --- /dev/null +++ b/src/main/resources/openapi/models/security/user-principal.yaml @@ -0,0 +1,2 @@ +UserPrincipal: + type: object \ No newline at end of file diff --git a/src/main/resources/openapi/models/user/user-response.yaml b/src/main/resources/openapi/models/user/user-response.yaml new file mode 100644 index 0000000..551f0ca --- /dev/null +++ b/src/main/resources/openapi/models/user/user-response.yaml @@ -0,0 +1,16 @@ +UserResponseDTO: + type: object + properties: + id: + type: string + format: uuid + username: + type: string + osuId: + type: integer + isRestricted: + type: boolean + roles: + type: array + items: + $ref: '../roles/role-response.yaml#/RoleResponseDTO' \ No newline at end of file diff --git a/src/main/resources/openapi/openapi.yaml b/src/main/resources/openapi/openapi.yaml index a43db3e..cf04973 100644 --- a/src/main/resources/openapi/openapi.yaml +++ b/src/main/resources/openapi/openapi.yaml @@ -1,15 +1,14 @@ openapi: 3.0.2 info: - title: Sample OpenAPI Specification for Spring Boot API + title: Auth microservice version: 1.0.0 - description: A sample Spring Boot API using OpenAPI specification + description: OpenAPI specification for Auth microservice contact: name: AimCup email: aimcupdev@gmail.com -tags: - - name: Sample-OpenApi - description: Sample-OpenApi paths: - /examples: - $ref: './paths/example/example.yaml' + /me: + $ref: './paths/user/user.yaml' + /{osuId}: + $ref: './paths/user/user-by-osu-id.yaml' diff --git a/src/main/resources/openapi/paths/example/example.yaml b/src/main/resources/openapi/paths/example/example.yaml deleted file mode 100644 index 064f438..0000000 --- a/src/main/resources/openapi/paths/example/example.yaml +++ /dev/null @@ -1,33 +0,0 @@ -get: - tags: - - Example - summary: Get example object - operationId: get-examples - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '../../models/example/example-data-response.yaml#/ExampleDataResponse' - -post: - tags: - - Example - summary: Add example object - operationId: add-new-examples - requestBody: - content: - application/json: - schema: - $ref: '../../models/example/example-data-request.yaml#/ExampleDataRequest' - responses: - '200': - description: OK - content: - application/json: - schema: - type: string - description: Created message \ No newline at end of file diff --git a/src/main/resources/openapi/paths/user/user-by-osu-id.yaml b/src/main/resources/openapi/paths/user/user-by-osu-id.yaml new file mode 100644 index 0000000..75de0a0 --- /dev/null +++ b/src/main/resources/openapi/paths/user/user-by-osu-id.yaml @@ -0,0 +1,19 @@ +get: + tags: + - User + summary: Get user by osu! ID + operationId: get-user-by-osu-id + parameters: + - name: osuId + in: path + required: true + schema: + type: string + description: osu! ID + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../../models/user/user-response.yaml#/UserResponseDTO' \ No newline at end of file diff --git a/src/main/resources/openapi/paths/user/user.yaml b/src/main/resources/openapi/paths/user/user.yaml new file mode 100644 index 0000000..49b5d64 --- /dev/null +++ b/src/main/resources/openapi/paths/user/user.yaml @@ -0,0 +1,13 @@ +get: + tags: + - User + summary: Get user by JWT token + operationId: me + description: Get user by JWT token + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '../../models/user/user-response.yaml#/UserResponseDTO' \ No newline at end of file diff --git a/src/test/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImplTest.java b/src/test/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImplTest.java new file mode 100644 index 0000000..a67b408 --- /dev/null +++ b/src/test/java/xyz/aimcup/auth/feign/osu/impl/OsuClientImplTest.java @@ -0,0 +1,99 @@ +package xyz.aimcup.auth.feign.osu.impl; + +import feign.FeignException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.web.server.ResponseStatusException; +import xyz.aimcup.auth.feign.osu.FeignOsuClient; +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@ExtendWith(MockitoExtension.class) +class OsuClientImplTest { + + @Mock + private FeignOsuClient feignOsuClient; + + @InjectMocks + private OsuClientImpl osuClient; + + @Test + @DisplayName("Should return user by id from osu! API") + void shouldReturnUserByIdFromOsuApi() { + // given + final String id = "3660794"; + + OsuUserExtended osuUserExtended = new OsuUserExtended(); + osuUserExtended.setId(Long.valueOf(id)); + + // when + Mockito.when(feignOsuClient.getUserById(id)).thenReturn(osuUserExtended); + OsuUserExtended expectedUser = osuClient.getUserById(id); + + // then + assertThat(expectedUser.getId()).isEqualTo(osuUserExtended.getId()); + } + + @Test + @DisplayName("Should throw exception when user not found") + void shouldThrowExceptionWhenUserNotFound() { + // given + final String id = "3660794"; + + final FeignException.NotFound feignExceptionNotFound = Mockito.mock(FeignException.NotFound.class); + + // when + Mockito.when(feignOsuClient.getUserById(id)).thenThrow(feignExceptionNotFound); + + // then + assertThatThrownBy(() -> osuClient.getUserById(id)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("User with id " + id + " not found") + .hasFieldOrPropertyWithValue("status", HttpStatus.NOT_FOUND); + } + + @Test + @DisplayName("Should throw exception when bad request") + void shouldThrowExceptionWhenBadRequest() { + // given + final String id = "3660794"; + + final FeignException.BadRequest feignExceptionBadRequest = Mockito.mock(FeignException.BadRequest.class); + + // when + Mockito.when(feignOsuClient.getUserById(id)).thenThrow(feignExceptionBadRequest); + + // then + assertThatThrownBy(() -> osuClient.getUserById(id)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("Bad request") + .hasFieldOrPropertyWithValue("status", HttpStatus.BAD_REQUEST); + } + + @Test + @DisplayName("Should throw exception when external API call failed") + void shouldThrowExceptionWhenExternalApiCallFailed() { + // given + final String id = "3660794"; + + final FeignException feignException = Mockito.mock(FeignException.class); + + // when + Mockito.when(feignOsuClient.getUserById(id)).thenThrow(feignException); + + // then + assertThatThrownBy(() -> osuClient.getUserById(id)) + .isInstanceOf(ResponseStatusException.class) + .hasMessageContaining("External API call failed") + .hasFieldOrPropertyWithValue("status", HttpStatus.INTERNAL_SERVER_ERROR); + } + +} \ No newline at end of file diff --git a/src/test/java/xyz/aimcup/auth/service/impl/UserServiceImplTest.java b/src/test/java/xyz/aimcup/auth/service/impl/UserServiceImplTest.java new file mode 100644 index 0000000..f564d93 --- /dev/null +++ b/src/test/java/xyz/aimcup/auth/service/impl/UserServiceImplTest.java @@ -0,0 +1,124 @@ +package xyz.aimcup.auth.service.impl; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import xyz.aimcup.auth.data.entity.User; +import xyz.aimcup.auth.feign.osu.OsuClient; +import xyz.aimcup.auth.feign.osu.model.OsuUserExtended; +import xyz.aimcup.auth.mapper.user.UserMapper; +import xyz.aimcup.auth.properties.AimCupProperties; +import xyz.aimcup.auth.properties.KeycloakProperties; +import xyz.aimcup.auth.service.KeycloakService; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@SpringBootTest(properties = { + "aimcup.keycloak.realm=aimcup_realm" +}, + classes = UserServiceImpl.class) +class UserServiceImplTest { + + @MockBean + private KeycloakService keycloakService; + + @MockBean + private OsuClient osuClient; + + @MockBean + private UserMapper userMapper; + + @MockBean + private AimCupProperties aimCupProperties; + + @Autowired + private UserServiceImpl userService; + + @Test + @DisplayName("Should return User when user exists in Keycloak") + void shouldReturnUserWhenUserExistsInKeycloak() { + // given + final String osuId = "3660794"; + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setId(osuId); + userRepresentation.setUsername(osuId); + + final Keycloak keycloak = mock(Keycloak.class); + RealmResource resource = mock(RealmResource.class); + UsersResource usersResource = mock(UsersResource.class); + KeycloakProperties keycloakProperties = mock(KeycloakProperties.class); + + final User user = User.builder() + .id(UUID.randomUUID()) + .osuId(osuId) + .build(); + + // when + when(aimCupProperties.getKeycloak()).thenReturn(keycloakProperties); + when(keycloakProperties.getRealm()).thenReturn("aimcup_realm"); + when(keycloakService.getKeycloak()).thenReturn(keycloak); + when(keycloak.realm("aimcup_realm")).thenReturn(resource); + when(resource.users()).thenReturn(usersResource); + when(usersResource.searchByUsername(osuId, true)).thenReturn(List.of(userRepresentation)); + when(userMapper.userRepresentationToUser(userRepresentation)).thenReturn(user); + + User expectedUser = userService.getUserByOsuId(osuId); + + // then + assertThat(expectedUser.getOsuId()).isEqualTo(user.getOsuId()); + } + + @Test + @DisplayName("Should create User when user does not exist in Keycloak") + void shouldCreateUserWhenUserDoesNotExistInKeycloak() { + // given + final String osuId = "3660794"; + UserRepresentation userRepresentation = new UserRepresentation(); + userRepresentation.setId(osuId); + userRepresentation.setUsername(osuId); + + final Keycloak keycloak = mock(Keycloak.class); + RealmResource resource = mock(RealmResource.class); + UsersResource usersResource = mock(UsersResource.class); + OsuUserExtended osuUserExtended = mock(OsuUserExtended.class); + KeycloakProperties keycloakProperties = mock(KeycloakProperties.class); + + final User user = User.builder() + .id(UUID.randomUUID()) + .osuId(osuId) + .build(); + + // when + when(aimCupProperties.getKeycloak()).thenReturn(keycloakProperties); + when(keycloakProperties.getRealm()).thenReturn("aimcup_realm"); + when(keycloakService.getKeycloak()).thenReturn(keycloak); + when(keycloak.realm("aimcup_realm")).thenReturn(resource); + when(resource.users()).thenReturn(usersResource); + when(usersResource.searchByUsername(osuId, true)).thenReturn(List.of()); + + when(osuClient.getUserById(osuId)).thenReturn(osuUserExtended); + when(keycloakService.createUser(osuUserExtended)).thenReturn(userRepresentation); + + when(userMapper.userRepresentationToUser(userRepresentation)).thenReturn(user); + + User expectedUser = userService.getUserByOsuId(osuId); + + // then + assertThat(expectedUser.getOsuId()).isEqualTo(user.getOsuId()); + } + +} \ No newline at end of file diff --git a/src/test/java/xyz/aimcup/example/ExampleApplicationIT.java b/src/test/java/xyz/aimcup/example/ExampleApplicationIT.java deleted file mode 100644 index 2dcae3a..0000000 --- a/src/test/java/xyz/aimcup/example/ExampleApplicationIT.java +++ /dev/null @@ -1,15 +0,0 @@ -package xyz.aimcup.example; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import xyz.aimcup.example.reusablecontainers.DatabaseContainerIT; - -@SpringBootTest -class ExampleApplicationIT extends DatabaseContainerIT { - - @Test - @SuppressWarnings("squid:S2699") - void contextLoads() { - } - -} diff --git a/src/test/java/xyz/aimcup/example/controller/ExampleControllerIT.java b/src/test/java/xyz/aimcup/example/controller/ExampleControllerIT.java deleted file mode 100644 index 7b583a3..0000000 --- a/src/test/java/xyz/aimcup/example/controller/ExampleControllerIT.java +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.aimcup.example.controller; - - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import xyz.aimcup.generated.model.ExampleDataRequest; -import xyz.aimcup.generated.model.ExampleDataResponse; -import xyz.aimcup.example.reusablecontainers.DatabaseContainerIT; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@ExtendWith(MockitoExtension.class) -class ExampleControllerIT extends DatabaseContainerIT { - @Autowired - private ExampleController exampleController; - - @Test - void shouldAddExampleToDatabase() { - // given - ExampleDataRequest exampleDataRequest = new ExampleDataRequest(); - exampleDataRequest.setData("example data"); - - // when - String response = exampleController.addNewExamples(exampleDataRequest).getBody(); - - // then - assertThat(response).isEqualTo("Example added"); - } - - @Test - void shouldFindOnlyOneExampleInDatabase() { - // given - ExampleDataRequest exampleDataRequest = new ExampleDataRequest(); - exampleDataRequest.setData("example data 2"); - - // when - exampleController.addNewExamples(exampleDataRequest).getBody(); - exampleController.addNewExamples(exampleDataRequest).getBody(); - List exampleList = exampleController.getExamples().getBody(); - - // then - assertThat(exampleList).hasSizeGreaterThan(1); - } -} \ No newline at end of file diff --git a/src/test/java/xyz/aimcup/example/reusablecontainers/DatabaseContainerIT.java b/src/test/java/xyz/aimcup/example/reusablecontainers/DatabaseContainerIT.java deleted file mode 100644 index 33ffbf5..0000000 --- a/src/test/java/xyz/aimcup/example/reusablecontainers/DatabaseContainerIT.java +++ /dev/null @@ -1,31 +0,0 @@ -package xyz.aimcup.example.reusablecontainers; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public abstract class DatabaseContainerIT { - - private static final String POSTGRES_VERSION = "postgres:latest"; - private static final PostgreSQLContainer postgreSQLContainer; - - static { - postgreSQLContainer = - new PostgreSQLContainer<>(DockerImageName.parse(POSTGRES_VERSION)) - .withDatabaseName("test-db") - .withUsername("test-username") - .withPassword("test-password"); - - postgreSQLContainer.start(); - } - - @DynamicPropertySource - static void datasourceConfig(DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", postgreSQLContainer::getJdbcUrl); - registry.add("spring.datasource.password", postgreSQLContainer::getPassword); - registry.add("spring.datasource.username", postgreSQLContainer::getUsername); - } -} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml deleted file mode 100644 index 4875cf1..0000000 --- a/src/test/resources/application-test.yml +++ /dev/null @@ -1,27 +0,0 @@ -eureka: - client: - enabled: false -server: - servlet: - context-path: /example-path - port: 0 -spring: - application: - name: example-microservice - datasource: - driver-class-name: org.postgresql.Driver - hikari: - connection-timeout: 10000 - maximum-pool-size: 10 - idle-timeout: 5000 - max-lifetime: 1000 - auto-commit: true - flyway: - locations: classpath:db/migration - enabled: true - clean-disabled: false - jpa: - show-sql: true - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect