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