From 3f408d879342c48d0ced38190bac6ed9ee4d1255 Mon Sep 17 00:00:00 2001 From: Kamal Mohammed Date: Fri, 6 Jun 2025 16:57:02 -0600 Subject: [PATCH] GRAD2-3282 - Apply client authentication to GRAD-STUDENT-GRADUATION-API --- ...ld.from.developer.branch.deploy.to.dev.yml | 17 ++ .../build.from.main.branch.deploy.to.dev.yml | 17 ++ ...uild.from.release.branch.deploy.to.dev.yml | 17 ++ .github/workflows/deploy_latest_to_test.yml | 17 ++ .github/workflows/deploy_prod.yml | 19 +- .github/workflows/deploy_test.yml | 19 +- .gitignore | 3 +- api/pom.xml | 4 + .../config/FlywayMigrationStrategyImpl.java | 10 +- .../config/GradStudentGraduationConfig.java | 53 +----- .../config/RestErrorHandler.java | 20 +- .../config/RestWebClient.java | 100 ++++++++++ .../config/SwaggerConfig.java | 34 ++++ .../config/WebSecurityConfiguration.java | 1 - .../controller/AlgorithmRuleController.java | 45 +++-- .../LettergradeSpecialcaseController.java | 41 ++-- .../TranscriptMessageController.java | 24 +-- .../UndoCompletionReasonController.java | 39 ++-- .../exception/ServiceException.java | 39 ++++ .../service/AlgorithmRuleService.java | 95 +++++----- .../service/LetterGradeService.java | 39 +--- .../service/RESTService.java | 130 +++++++++++++ .../service/SpecialCaseService.java | 5 - .../StudentUndoCompletionReasonService.java | 9 - .../service/TranscriptMessageService.java | 9 - .../service/UndoCompletionReasonService.java | 8 - ...EducGradStudentGraduationApiConstants.java | 3 + .../api/studentgraduation/util/LogHelper.java | 4 +- api/src/main/resources/application.yaml | 17 ++ .../AlgorithmRuleControllerTest.java | 20 +- .../service/AlgorithmRuleServiceTest.java | 19 +- .../service/LetterGradeServiceTest.java | 9 +- .../service/RESTServiceGETTest.java | 162 ++++++++++++++++ .../service/RESTServicePOSTTest.java | 129 +++++++++++++ .../service/SpecialCaseServiceTest.java | 6 + ...tudentUndoCompletionReasonServiceTest.java | 7 +- .../service/TranscriptMessageServiceTest.java | 7 +- .../UndoCompletionReasonServiceTest.java | 10 +- api/src/test/resources/application.yaml | 2 + ...=> Read Algorithm Data by ProgramCode.bru} | 2 +- ...ad All Data required by Grad Algorithm.bru | 11 ++ tools/config/clients-and-scopes.js | 178 ++++++++++++++++++ tools/config/clients-config.json | 24 +++ tools/openshift/api.dc.yaml | 2 + tools/openshift/clients.json | 3 + tools/openshift/fetch-and-create-secrets.js | 118 ++++++++++++ 46 files changed, 1253 insertions(+), 294 deletions(-) create mode 100644 api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestWebClient.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/SwaggerConfig.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/studentgraduation/exception/ServiceException.java create mode 100644 api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/RESTService.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServiceGETTest.java create mode 100644 api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServicePOSTTest.java rename tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/{Read All Data required by Grad Algorithm.bru => Read Algorithm Data by ProgramCode.bru} (77%) create mode 100644 tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru create mode 100644 tools/config/clients-and-scopes.js create mode 100644 tools/config/clients-config.json create mode 100644 tools/openshift/clients.json create mode 100644 tools/openshift/fetch-and-create-secrets.js diff --git a/.github/workflows/build.from.developer.branch.deploy.to.dev.yml b/.github/workflows/build.from.developer.branch.deploy.to.dev.yml index b098474..06abf35 100644 --- a/.github/workflows/build.from.developer.branch.deploy.to.dev.yml +++ b/.github/workflows/build.from.developer.branch.deploy.to.dev.yml @@ -10,6 +10,9 @@ env: COMMON_NAMESPACE: ${{ secrets.COMMON_NAMESPACE }} NAMESPACE: ${{ secrets.GRAD_NAMESPACE }} BUSINESS_NAMESPACE: ${{ secrets.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: dev # đŸ–Šī¸ EDIT to change the image registry settings. # Registries such as GHCR, Quay.io, and Docker Hub are supported. @@ -95,6 +98,20 @@ jobs: username: ${{ env.IMAGE_REGISTRY_USER }} password: ${{ env.IMAGE_REGISTRY_PASSWORD }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + # The path the image was pushed to is now stored in ${{ steps.push-image.outputs.registry-path }} - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 diff --git a/.github/workflows/build.from.main.branch.deploy.to.dev.yml b/.github/workflows/build.from.main.branch.deploy.to.dev.yml index 84674c2..9ffffa0 100644 --- a/.github/workflows/build.from.main.branch.deploy.to.dev.yml +++ b/.github/workflows/build.from.main.branch.deploy.to.dev.yml @@ -10,6 +10,9 @@ env: COMMON_NAMESPACE: ${{ vars.COMMON_NAMESPACE }} GRAD_NAMESPACE: ${{ vars.GRAD_NAMESPACE }} BUSINESS_NAMESPACE: ${{ vars.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: dev # đŸ–Šī¸ EDIT to change the image registry settings. # Registries such as GHCR, Quay.io, and Docker Hub are supported. @@ -81,6 +84,20 @@ jobs: username: ${{ env.IMAGE_REGISTRY_USER }} password: ${{ env.IMAGE_REGISTRY_PASSWORD }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + # The path the image was pushed to is now stored in ${{ steps.push-image.outputs.registry-path }} - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 diff --git a/.github/workflows/build.from.release.branch.deploy.to.dev.yml b/.github/workflows/build.from.release.branch.deploy.to.dev.yml index 90bdb4b..edbb9b7 100644 --- a/.github/workflows/build.from.release.branch.deploy.to.dev.yml +++ b/.github/workflows/build.from.release.branch.deploy.to.dev.yml @@ -10,6 +10,9 @@ env: COMMON_NAMESPACE: ${{ vars.COMMON_NAMESPACE }} GRAD_NAMESPACE: ${{ vars.GRAD_NAMESPACE }} BUSINESS_NAMESPACE: ${{ vars.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: dev # đŸ–Šī¸ EDIT to change the image registry settings. # Registries such as GHCR, Quay.io, and Docker Hub are supported. @@ -89,6 +92,20 @@ jobs: username: ${{ env.IMAGE_REGISTRY_USER }} password: ${{ env.IMAGE_REGISTRY_PASSWORD }} + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + # The path the image was pushed to is now stored in ${{ steps.push-image.outputs.registry-path }} - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 diff --git a/.github/workflows/deploy_latest_to_test.yml b/.github/workflows/deploy_latest_to_test.yml index 71a4cd6..6c34c46 100644 --- a/.github/workflows/deploy_latest_to_test.yml +++ b/.github/workflows/deploy_latest_to_test.yml @@ -9,6 +9,9 @@ env: OPENSHIFT_NAMESPACE: ${{ vars.GRAD_NAMESPACE }}-test COMMON_NAMESPACE: ${{ vars.COMMON_NAMESPACE }} BUSINESS_NAMESPACE: ${{ vars.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: test SPRING_BOOT_IMAGE_NAME: educ-grad-student-graduation-api @@ -38,6 +41,20 @@ jobs: - name: Check out repository uses: actions/checkout@v4 + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 with: diff --git a/.github/workflows/deploy_prod.yml b/.github/workflows/deploy_prod.yml index d9a9f0e..4207ffc 100644 --- a/.github/workflows/deploy_prod.yml +++ b/.github/workflows/deploy_prod.yml @@ -9,6 +9,9 @@ env: OPENSHIFT_NAMESPACE: ${{ vars.GRAD_NAMESPACE }}-prod COMMON_NAMESPACE: ${{ vars.COMMON_NAMESPACE }} BUSINESS_NAMESPACE: ${{ vars.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: prod SPRING_BOOT_IMAGE_NAME: educ-grad-student-graduation-api @@ -42,6 +45,20 @@ jobs: uses: actions-ecosystem/action-get-latest-tag@v1 id: get-latest-tag + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 with: @@ -75,7 +92,7 @@ jobs: -p MAX_MEM=${{ env.MAX_MEM }} | oc apply -f - # UPDATE Configmaps - curl -s https://raw.githubusercontent.com/bcgov/${{ env.REPO_NAME }}/${{ env.BRANCH }}/tools/config/update-configmap.sh \ + curl -s https://raw.githubusercontent.com/bcgov/${{ env.REPO_NAME }}/${{ steps.get-latest-tag.outputs.tag }}/tools/config/update-configmap.sh \ | bash /dev/stdin \ prod \ ${{ env.REPO_NAME }} \ diff --git a/.github/workflows/deploy_test.yml b/.github/workflows/deploy_test.yml index 23498a0..9b7604a 100644 --- a/.github/workflows/deploy_test.yml +++ b/.github/workflows/deploy_test.yml @@ -9,6 +9,9 @@ env: OPENSHIFT_NAMESPACE: ${{ vars.GRAD_NAMESPACE }}-test COMMON_NAMESPACE: ${{ vars.COMMON_NAMESPACE }} BUSINESS_NAMESPACE: ${{ vars.GRAD_BUSINESS_NAMESPACE }} + KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }} + KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }} + TARGET_ENV: test SPRING_BOOT_IMAGE_NAME: educ-grad-student-graduation-api @@ -42,6 +45,20 @@ jobs: uses: actions-ecosystem/action-get-latest-tag@v1 id: get-latest-tag + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: npm install axios + + - name: Create/Update clients + run: node ./tools/config/clients-and-scopes.js + + - name: Create/Update secrets + run: node ./tools/openshift/fetch-and-create-secrets.js + - name: Install oc uses: redhat-actions/openshift-tools-installer@v1 with: @@ -75,7 +92,7 @@ jobs: -p MAX_MEM=${{ env.MAX_MEM }} | oc apply -f - # UPDATE Configmaps - curl -s https://raw.githubusercontent.com/bcgov/${{ env.REPO_NAME }}/${{ env.BRANCH }}/tools/config/update-configmap.sh \ + curl -s https://raw.githubusercontent.com/bcgov/${{ env.REPO_NAME }}/${{ steps.get-latest-tag.outputs.tag }}/tools/config/update-configmap.sh \ | bash /dev/stdin \ test \ ${{ env.REPO_NAME }} \ diff --git a/.gitignore b/.gitignore index 1ca7b04..05d2516 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,5 @@ build/ .vscode/ ### Local dev ### -**/application-local.yaml \ No newline at end of file +**/application-local.yaml +**/generate-local-env.sh \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml index ee96613..805166e 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -68,6 +68,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-validation + org.apache.logging.log4j diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/FlywayMigrationStrategyImpl.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/FlywayMigrationStrategyImpl.java index 97652d9..bd8362d 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/FlywayMigrationStrategyImpl.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/FlywayMigrationStrategyImpl.java @@ -1,24 +1,24 @@ package ca.bc.gov.educ.api.studentgraduation.config; +import lombok.extern.slf4j.Slf4j; import org.flywaydb.core.Flyway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; import org.springframework.stereotype.Component; +@Slf4j @Component public class FlywayMigrationStrategyImpl implements FlywayMigrationStrategy { - private static Logger logger = LoggerFactory.getLogger(FlywayMigrationStrategyImpl.class); - @Override public void migrate(Flyway flyway) { if (!flyway.validateWithResult().validationSuccessful) { flyway.repair(); } - logger.info("\n"); - logger.info("************FLYWAY-MIGRATE**********"); + log.info("\n"); + log.info("************FLYWAY-MIGRATE**********"); flyway.migrate(); - logger.info("************FLYWAY-MIGRATE-END**********\n\n"); + log.info("************FLYWAY-MIGRATE-END**********\n\n"); } } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/GradStudentGraduationConfig.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/GradStudentGraduationConfig.java index fd38397..af225d7 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/GradStudentGraduationConfig.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/GradStudentGraduationConfig.java @@ -2,17 +2,10 @@ import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; import ca.bc.gov.educ.api.studentgraduation.util.LogHelper; -import ca.bc.gov.educ.api.studentgraduation.util.ThreadLocalStateUtil; import org.modelmapper.ModelMapper; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.ClientRequest; -import org.springframework.web.reactive.function.client.ExchangeFilterFunction; -import org.springframework.web.reactive.function.client.WebClient; -import reactor.netty.http.client.HttpClient; @Configuration public class GradStudentGraduationConfig { @@ -28,50 +21,6 @@ public GradStudentGraduationConfig(EducGradStudentGraduationApiConstants constan @Bean public ModelMapper modelMapper() { - - ModelMapper modelMapper = new ModelMapper(); - //modelMapper.typeMap(GradProgramEntity.class, GradProgram.class); - //modelMapper.typeMap(GradProgram.class, GradProgramEntity.class); - return modelMapper; - } - - @Bean - public WebClient webClient() { - HttpClient client = HttpClient.create(); - client.warmup().block(); - return WebClient.builder() - .filter(setRequestHeaders()) - .filter(this.log()) - .build(); - } - - @Bean - public RestTemplate restTemplate(RestTemplateBuilder builder) { - return builder.build(); - } - - private ExchangeFilterFunction setRequestHeaders() { - return (clientRequest, next) -> { - ClientRequest modifiedRequest = ClientRequest.from(clientRequest) - .header(EducGradStudentGraduationApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()) - .header(EducGradStudentGraduationApiConstants.USER_NAME, ThreadLocalStateUtil.getCurrentUser()) - .header(EducGradStudentGraduationApiConstants.REQUEST_SOURCE, EducGradStudentGraduationApiConstants.API_NAME) - .build(); - return next.exchange(modifiedRequest); - }; + return new ModelMapper(); } - - private ExchangeFilterFunction log() { - return (clientRequest, next) -> next - .exchange(clientRequest) - .doOnNext((clientResponse -> logHelper.logClientHttpReqResponseDetails( - clientRequest.method(), - clientRequest.url().toString(), - clientResponse.statusCode().value(), - clientRequest.headers().get(EducGradStudentGraduationApiConstants.CORRELATION_ID), - clientRequest.headers().get(EducGradStudentGraduationApiConstants.REQUEST_SOURCE), - constants.isSplunkLogHelperEnabled()) - )); - } - } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestErrorHandler.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestErrorHandler.java index f576347..84931c3 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestErrorHandler.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestErrorHandler.java @@ -1,8 +1,8 @@ package ca.bc.gov.educ.api.studentgraduation.config; +import lombok.extern.slf4j.Slf4j; import org.hibernate.dialect.lock.OptimisticEntityLockException; import org.hibernate.exception.ConstraintViolationException; -import org.jboss.logging.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataRetrievalFailureException; @@ -20,10 +20,10 @@ import ca.bc.gov.educ.api.studentgraduation.util.GradBusinessRuleException; import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; +@Slf4j @ControllerAdvice public class RestErrorHandler extends ResponseEntityExceptionHandler { - private static final Logger LOGGER = Logger.getLogger(RestErrorHandler.class); public static final String ERROR_MSG = "Illegal argument ERROR IS:"; @Autowired @@ -31,7 +31,7 @@ public class RestErrorHandler extends ResponseEntityExceptionHandler { @ExceptionHandler(value = { IllegalArgumentException.class, IllegalStateException.class }) protected ResponseEntity handleConflict(RuntimeException ex, WebRequest request) { - LOGGER.error(ERROR_MSG + ex.getClass().getName(), ex); + log.error(ERROR_MSG + ex.getClass().getName(), ex); ApiResponseModel reponse = ApiResponseModel.ERROR(null, ex.getLocalizedMessage()); validation.ifErrors(errorList -> reponse.addErrorMessages(errorList)); validation.ifWarnings(warningList -> reponse.addWarningMessages(warningList)); @@ -41,7 +41,7 @@ protected ResponseEntity handleConflict(RuntimeException ex, WebRequest @ExceptionHandler(value = { JpaObjectRetrievalFailureException.class, DataRetrievalFailureException.class }) protected ResponseEntity handleEntityNotFound(RuntimeException ex, WebRequest request) { - LOGGER.error("JPA ERROR IS: " + ex.getClass().getName(), ex); + log.error("JPA ERROR IS: " + ex.getClass().getName(), ex); validation.clear(); return new ResponseEntity<>(ApiResponseModel.ERROR(null, ex.getLocalizedMessage()), HttpStatus.BAD_REQUEST); } @@ -49,7 +49,7 @@ protected ResponseEntity handleEntityNotFound(RuntimeException ex, WebRe @ExceptionHandler(value = { AccessDeniedException.class }) protected ResponseEntity handleAuthorizationErrors(Exception ex, WebRequest request) { - LOGGER.error("Authorization error EXCETPION IS: " + ex.getClass().getName()); + log.error("Authorization error EXCETPION IS: " + ex.getClass().getName()); String message = "You are not authorized to access this resource."; validation.clear(); return new ResponseEntity<>(ApiResponseModel.ERROR(null, message), HttpStatus.FORBIDDEN); @@ -70,8 +70,8 @@ protected ResponseEntity handleIrisBusinessException(Exception ex, WebRe @ExceptionHandler(value = { OptimisticEntityLockException.class }) protected ResponseEntity handleOptimisticEntityLockException(OptimisticEntityLockException ex, WebRequest request) { - LOGGER.error("EXCEPTION IS: " + ex.getClass().getName(), ex); - LOGGER.error(ERROR_MSG + ex.getClass().getName(), ex); + log.error("EXCEPTION IS: " + ex.getClass().getName(), ex); + log.error(ERROR_MSG + ex.getClass().getName(), ex); ApiResponseModel response = ApiResponseModel.ERROR(null); validation.ifErrors(errorList -> response.addErrorMessages(errorList)); validation.ifWarnings(warningList -> response.addWarningMessages(warningList)); @@ -85,7 +85,7 @@ protected ResponseEntity handleOptimisticEntityLockException(OptimisticE @ExceptionHandler(value = { DataIntegrityViolationException.class }) protected ResponseEntity handleSQLException(DataIntegrityViolationException ex, WebRequest request) { - LOGGER.error("DATA INTEGRITY VIOLATION IS: " + ex.getClass().getName(), ex); + log.error("DATA INTEGRITY VIOLATION IS: " + ex.getClass().getName(), ex); String msg = ex.getLocalizedMessage(); Throwable cause = ex.getCause(); @@ -107,8 +107,8 @@ protected ResponseEntity handleSQLException(DataIntegrityViolationExcept @ExceptionHandler(value = { Exception.class }) protected ResponseEntity handleUncaughtException(Exception ex, WebRequest request) { - LOGGER.error("EXCEPTION IS: " + ex.getClass().getName(), ex); - LOGGER.error(ERROR_MSG + ex.getClass().getName(), ex); + log.error("EXCEPTION IS: " + ex.getClass().getName(), ex); + log.error(ERROR_MSG + ex.getClass().getName(), ex); ApiResponseModel response = ApiResponseModel.ERROR(null); validation.ifErrors(errorList -> response.addErrorMessages(errorList)); validation.ifWarnings(warningList -> response.addWarningMessages(warningList)); diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestWebClient.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestWebClient.java new file mode 100644 index 0000000..642e446 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/RestWebClient.java @@ -0,0 +1,100 @@ +package ca.bc.gov.educ.api.studentgraduation.config; + +import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; +import ca.bc.gov.educ.api.studentgraduation.util.LogHelper; +import ca.bc.gov.educ.api.studentgraduation.util.ThreadLocalStateUtil; +import io.netty.handler.logging.LogLevel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.security.oauth2.client.*; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.DefaultUriBuilderFactory; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; + +import java.time.Duration; + +@Configuration +@Profile("!test") +public class RestWebClient { + + EducGradStudentGraduationApiConstants constants; + private final HttpClient httpClient; + + @Autowired + public RestWebClient(EducGradStudentGraduationApiConstants constants) { + this.constants = constants; + this.httpClient = HttpClient.create(ConnectionProvider.create("student-graduation-api")).compress(true) + .resolver(spec -> spec.queryTimeout(Duration.ofMillis(200)).trace("DNS", LogLevel.TRACE)); + this.httpClient.warmup().block(); + } + + @Primary + @Bean("studentGraduationApiClient") + public WebClient getCourseApiClientWebClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction filter = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + filter.setDefaultClientRegistrationId("student-graduation-api-client"); + DefaultUriBuilderFactory defaultUriBuilderFactory = new DefaultUriBuilderFactory(); + defaultUriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + return WebClient.builder() + .uriBuilderFactory(defaultUriBuilderFactory) + .filter(setRequestHeaders()) + .exchangeStrategies(ExchangeStrategies + .builder() + .codecs(codecs -> codecs + .defaultCodecs() + .maxInMemorySize(50 * 1024 * 1024)) + .build()) + .apply(filter.oauth2Configuration()) + .filter(this.log()) + .build(); + } + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientService clientService) { + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, clientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; + } + + private ExchangeFilterFunction setRequestHeaders() { + return (clientRequest, next) -> { + ClientRequest modifiedRequest = ClientRequest.from(clientRequest) + .header(EducGradStudentGraduationApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID()) + .header(EducGradStudentGraduationApiConstants.USER_NAME, ThreadLocalStateUtil.getCurrentUser()) + .header(EducGradStudentGraduationApiConstants.REQUEST_SOURCE, EducGradStudentGraduationApiConstants.API_NAME) + .build(); + return next.exchange(modifiedRequest); + }; + } + + private ExchangeFilterFunction log() { + return (clientRequest, next) -> next + .exchange(clientRequest) + .doOnNext((clientResponse -> LogHelper.logClientHttpReqResponseDetails( + clientRequest.method(), + clientRequest.url().toString(), + clientResponse.statusCode().value(), + clientRequest.headers().get(EducGradStudentGraduationApiConstants.CORRELATION_ID), + clientRequest.headers().get(EducGradStudentGraduationApiConstants.REQUEST_SOURCE), + constants.isSplunkLogHelperEnabled()) + )); + } + +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/SwaggerConfig.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/SwaggerConfig.java new file mode 100644 index 0000000..16f3e11 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/SwaggerConfig.java @@ -0,0 +1,34 @@ +package ca.bc.gov.educ.api.studentgraduation.config; + +import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.OAuthFlow; +import io.swagger.v3.oas.models.security.OAuthFlows; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + private final EducGradStudentGraduationApiConstants constants; + + public SwaggerConfig(EducGradStudentGraduationApiConstants constants) { + this.constants = constants; + } + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .addSecurityItem(new SecurityRequirement().addList("OAUTH2")) + .schemaRequirement("OAUTH2", new SecurityScheme() + .type(SecurityScheme.Type.OAUTH2) + .scheme("bearer") + .bearerFormat("JWT") + .flows(new OAuthFlows() + .clientCredentials( new OAuthFlow() + .tokenUrl(constants.getTokenUrl()) + ))); + } +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/WebSecurityConfiguration.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/WebSecurityConfiguration.java index 2d9610d..a9f8152 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/WebSecurityConfiguration.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/config/WebSecurityConfiguration.java @@ -11,7 +11,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; - @Configuration @EnableMethodSecurity @EnableWebSecurity diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleController.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleController.java index 40943d7..0dd2b7f 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleController.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleController.java @@ -2,8 +2,7 @@ import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -13,7 +12,6 @@ import ca.bc.gov.educ.api.studentgraduation.model.dto.StudentGraduationAlgorithmData; import ca.bc.gov.educ.api.studentgraduation.service.AlgorithmRuleService; import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; import ca.bc.gov.educ.api.studentgraduation.util.PermissionsContants; import ca.bc.gov.educ.api.studentgraduation.util.ResponseHelper; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -23,58 +21,57 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +@Slf4j @CrossOrigin @RestController @RequestMapping(EducGradStudentGraduationApiConstants.GRAD_STUDENT_GRADUATION_ALGO_CONTROLLER_ROOT_MAPPING) @OpenAPIDefinition(info = @Info(title = "API for Algorithm Rule Data.", description = "This API contains endpoints for Algorithm Rule data.", version = "1"), security = {@SecurityRequirement(name = "OAUTH2", scopes = {"READ_GRAD_SPECIAL_CASE_DATA","READ_GRAD_LETTER_GRADE_DATA"})}) public class AlgorithmRuleController { - private static Logger logger = LoggerFactory.getLogger(AlgorithmRuleController.class); - - @Autowired AlgorithmRuleService algorithmRuleService; - - @Autowired - GradValidation validation; - - @Autowired ResponseHelper response; + + @Autowired + public AlgorithmRuleController(AlgorithmRuleService algorithmRuleService, ResponseHelper response) { + this.algorithmRuleService = algorithmRuleService; + this.response = response; + } @GetMapping(EducGradStudentGraduationApiConstants.GET_ALGORITHM_RULES_MAIN_PROGRAM) @PreAuthorize(PermissionsContants.READ_GRAD_ALGORITHM_RULES) - @Operation(summary = "Read All Grad Algorithm Rules by Program Code", description = "Read All Grad Algorithm Rules by Program Code which are active", tags = { "Algorithm" }) + @Operation(summary = "Read All Grad Algorithm Rules by Program Code", description = "Read All Grad Algorithm Rules by Program Code which are active", tags = { "Algorithm" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) - public ResponseEntity> getAlgorithmRulesList(@PathVariable String programCode) { - logger.debug("getAlgorithmRulesList : "); + public ResponseEntity> getAlgorithmRulesList(@PathVariable String programCode) { + log.debug("getAlgorithmRulesList : "); return response.GET(algorithmRuleService.getAlgorithmRulesList(programCode)); } @GetMapping(EducGradStudentGraduationApiConstants.GET_ALL_ALGORITHM_RULES_MAPPING) @PreAuthorize(PermissionsContants.READ_GRAD_ALGORITHM_RULES) - @Operation(summary = "Read All Grad Algorithm Rules", description = "Read All Grad Algorithm Rules", tags = { "Algorithm" }) + @Operation(summary = "Read All Grad Algorithm Rules", description = "Read All Grad Algorithm Rules", tags = { "Algorithm" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) - public ResponseEntity> getAllAlgorithmRulesList() { - logger.debug("getAllAlgorithmRulesList : "); + public ResponseEntity> getAllAlgorithmRulesList() { + log.debug("getAllAlgorithmRulesList : "); return response.GET(algorithmRuleService.getAllAlgorithmRulesList()); } @GetMapping(EducGradStudentGraduationApiConstants.GET_DATA_FOR_ALGORITHM_MAPPING) @PreAuthorize(PermissionsContants.READ_ALGORITHM_DATA) - @Operation(summary = "Read All Data required by Grad Algorithm", description = "Read All Data required by Grad Algorithm", tags = { "Algorithm" }) + @Operation(summary = "Read Grad Algorithm data by Program Code", description = "Read Grad Algorithm data by Program Code", tags = { "Algorithm" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) - public ResponseEntity getAllAlgorithmData(@PathVariable String programCode) { - logger.debug("getAllAlgorithmData : "); + public ResponseEntity getAllAlgorithmData(@PathVariable String programCode) { + log.debug("getAllAlgorithmData : "); return response.GET(algorithmRuleService.getAllAlgorithmData(programCode)); } @GetMapping(EducGradStudentGraduationApiConstants.GET_DATA_FOR_ALGORITHM_LIST_MAPPING) @PreAuthorize(PermissionsContants.READ_ALGORITHM_DATA) - @Operation(summary = "Read All Data required by Grad Algorithm", description = "Read All Data required by Grad Algorithm", tags = { "Algorithm" }) + @Operation(summary = "Read All Data required by Grad Algorithm", description = "Read All Data required by Grad Algorithm", tags = { "Algorithm" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) - public ResponseEntity> getAllAlgorithmDataList(@RequestHeader(name="Authorization") String accessToken) { - logger.debug("getAllAlgorithmData : "); - return response.GET(algorithmRuleService.getAllAlgorithmDataList(accessToken.replace("Bearer ", ""))); + public ResponseEntity> getAllAlgorithmDataList() { + log.debug("getAllAlgorithmData : "); + return response.GET(algorithmRuleService.getAllAlgorithmDataList()); } } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/LettergradeSpecialcaseController.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/LettergradeSpecialcaseController.java index 811b25b..31926f3 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/LettergradeSpecialcaseController.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/LettergradeSpecialcaseController.java @@ -2,9 +2,8 @@ import java.util.List; +import lombok.extern.slf4j.Slf4j; import org.modelmapper.TypeToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -19,7 +18,6 @@ import ca.bc.gov.educ.api.studentgraduation.service.LetterGradeService; import ca.bc.gov.educ.api.studentgraduation.service.SpecialCaseService; import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; import ca.bc.gov.educ.api.studentgraduation.util.PermissionsContants; import ca.bc.gov.educ.api.studentgraduation.util.ResponseHelper; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -29,33 +27,31 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +@Slf4j @CrossOrigin @RestController @RequestMapping(EducGradStudentGraduationApiConstants.GRAD_STUDENT_GRADUATION_LGSC_CONTROLLER_ROOT_MAPPING) @OpenAPIDefinition(info = @Info(title = "API for Letter Grade and Special Case Data.", description = "This API contains endpoints for Letter Grade and Special Case data.", version = "1"), security = {@SecurityRequirement(name = "OAUTH2", scopes = {"READ_GRAD_SPECIAL_CASE_DATA","READ_GRAD_LETTER_GRADE_DATA"})}) public class LettergradeSpecialcaseController { - private static Logger logger = LoggerFactory.getLogger(LettergradeSpecialcaseController.class); - - @Autowired LetterGradeService letterGradeService; - - @Autowired - SpecialCaseService specialCaseService; - - @Autowired - GradValidation validation; - + SpecialCaseService specialCaseService; + ResponseHelper response; + @Autowired - ResponseHelper response; - + public LettergradeSpecialcaseController(LetterGradeService letterGradeService, SpecialCaseService specialCaseService, + ResponseHelper response) { + this.letterGradeService = letterGradeService; + this.specialCaseService = specialCaseService; + this.response = response; + } @GetMapping(value=EducGradStudentGraduationApiConstants.GET_ALL_SPECIAL_CASE_MAPPING,produces= {"application/json"}) @PreAuthorize(PermissionsContants.READ_GRAD_SPECIAL_CASE) @Operation(summary = "Find All Special Cases", description = "Get All Special Cases", tags = { "Independent" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "204", description = "NO CONTENT.")}) public ResponseEntity> getAllSpecialCases() { - logger.debug("getAllSpecialCases : "); + log.debug("getAllSpecialCases : "); List specialList = specialCaseService.getAllSpecialCaseList(); if(!specialList.isEmpty()) { return response.GET(specialList,new TypeToken>() {}.getType()); @@ -67,22 +63,21 @@ public ResponseEntity> getAllSpecialCases() { @Operation(summary = "Find a Specific Special Case", description = "Get a Special Cases", tags = { "Independent" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "204", description = "NO CONTENT.")}) @PreAuthorize(PermissionsContants.READ_GRAD_SPECIAL_CASE) - public ResponseEntity getSpecificSpecialCases(@PathVariable String specialCode) { - logger.debug("getSpecificSpecialCases : "); + public ResponseEntity getSpecificSpecialCases(@PathVariable String specialCode) { + log.debug("getSpecificSpecialCases : "); SpecialCase gradSpecialCase = specialCaseService.getSpecificSpecialCase(specialCode); if(gradSpecialCase != null) { return response.GET(gradSpecialCase) ; } return response.NO_CONTENT(); } - - + @GetMapping(value=EducGradStudentGraduationApiConstants.GET_ALL_LETTER_GRADE_MAPPING,produces= {"application/json"}) @PreAuthorize(PermissionsContants.READ_GRAD_LETTER_GRADE) @Operation(summary = "Find All Letter Grade", description = "Get All Letter Grades", tags = { "Independent" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "204", description = "NO CONTENT.")}) public List getAllLetterGrades() { - logger.debug("getAllLetterGrades : "); + log.debug("getAllLetterGrades : "); return letterGradeService.getAllLetterGradesList(); } @@ -90,8 +85,8 @@ public List getAllLetterGrades() { @Operation(summary = "Find a Specific Letter Grade", description = "Get a Letter Grade", tags = { "Independent" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) @PreAuthorize(PermissionsContants.READ_GRAD_LETTER_GRADE) - public LetterGrade getSpecificLetterGrade(@PathVariable String letterGrade) { - logger.debug("getSpecificLetterGrade : "); + public LetterGrade getSpecificLetterGrade(@PathVariable String letterGrade) { + log.debug("getSpecificLetterGrade : "); return letterGradeService.getSpecificLetterGrade(letterGrade); } } \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/TranscriptMessageController.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/TranscriptMessageController.java index 96cd916..5c7b851 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/TranscriptMessageController.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/TranscriptMessageController.java @@ -2,8 +2,7 @@ import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; @@ -16,7 +15,6 @@ import ca.bc.gov.educ.api.studentgraduation.model.dto.TranscriptMessage; import ca.bc.gov.educ.api.studentgraduation.service.TranscriptMessageService; import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; import ca.bc.gov.educ.api.studentgraduation.util.PermissionsContants; import ca.bc.gov.educ.api.studentgraduation.util.ResponseHelper; import io.swagger.v3.oas.annotations.OpenAPIDefinition; @@ -26,6 +24,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +@Slf4j @RestController @RequestMapping(EducGradStudentGraduationApiConstants.GRAD_STUDENT_GRADUATION_TRANSCRIPT_MESSAGE_CONTROLLER_ROOT_MAPPING) @CrossOrigin @@ -34,18 +33,15 @@ "READ_GRAD_MESSEGING_CODE_DATA" }) }) public class TranscriptMessageController { - @Autowired - private TranscriptMessageService transcriptMessageService; - - @Autowired - GradValidation validation; - - @Autowired + private final TranscriptMessageService transcriptMessageService; ResponseHelper response; - private static Logger logger = LoggerFactory.getLogger(TranscriptMessageController.class); + @Autowired + public TranscriptMessageController(TranscriptMessageService transcriptMessageService, ResponseHelper response) { + this.transcriptMessageService = transcriptMessageService; + this.response = response; + } - @GetMapping(EducGradStudentGraduationApiConstants.GET_ALL_GRAD_MESSAGING_MAPPING) @PreAuthorize(PermissionsContants.READ_GRAD_MESSAGING) @@ -53,7 +49,7 @@ public class TranscriptMessageController { "Graduation Messages" }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK") }) public ResponseEntity> getAllTranscriptMessageList() { - logger.debug("getAllTranscriptMessageList : "); + log.debug("getAllTranscriptMessageList : "); return response.GET(transcriptMessageService.getAllTranscriptMessageList()); } @@ -65,7 +61,7 @@ public ResponseEntity> getAllTranscriptMessageList() { @ApiResponse(responseCode = "204", description = "NO CONTENT") }) public ResponseEntity getSpecificTranscriptMessageCode(@PathVariable String pgmCode, @PathVariable String msgType) { - logger.debug("getSpecificTranscriptMessageCode : "); + log.debug("getSpecificTranscriptMessageCode : "); TranscriptMessage gradResponse = transcriptMessageService.getSpecificTranscriptMessageCode(pgmCode, msgType); if (gradResponse != null) { return response.GET(gradResponse); diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/UndoCompletionReasonController.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/UndoCompletionReasonController.java index 15de7d1..3d97567 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/UndoCompletionReasonController.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/controller/UndoCompletionReasonController.java @@ -5,8 +5,7 @@ import jakarta.validation.Valid; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -37,34 +36,36 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +@Slf4j @CrossOrigin @RestController @RequestMapping(EducGradStudentGraduationApiConstants.GRAD_STUDENT_GRADUATION_UNGRAD_REASON_CONTROLLER_ROOT_MAPPING) @OpenAPIDefinition(info = @Info(title = "API for Student and General Ungrad Reasons.", description = "This API is for Student and General Ungrad Reasons endpoints.", version = "1"), security = {@SecurityRequirement(name = "OAUTH2", scopes = {"READ_GRAD_STUDENT_UNGRAD_REASONS_DATA"})}) public class UndoCompletionReasonController { - private static Logger logger = LoggerFactory.getLogger(UndoCompletionReasonController.class); - - @Autowired StudentUndoCompletionReasonService studentUndoCompletionReasonService; - - @Autowired UndoCompletionReasonService ungradReasonService; - - @Autowired GradValidation validation; - - @Autowired ResponseHelper response; private static final String REASON_CODE="Reason Code"; + + @Autowired + public UndoCompletionReasonController(StudentUndoCompletionReasonService studentUndoCompletionReasonService, + UndoCompletionReasonService ungradReasonService, GradValidation validation, + ResponseHelper response) { + this.studentUndoCompletionReasonService = studentUndoCompletionReasonService; + this.ungradReasonService = ungradReasonService; + this.validation = validation; + this.response = response; + } @GetMapping(EducGradStudentGraduationApiConstants.GET_ALL_STUDENT_UNGRAD_MAPPING) @PreAuthorize(PermissionsContants.READ_GRAD_STUDENT_UNGRAD_REASONS_DATA) @Operation(summary = "Find Student Ungrad Reasons by Student ID", description = "Get Student Ungrad Reasons By Student ID", tags = { "Ungrad Reasons" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK")}) public ResponseEntity> getAllStudentUndoCompletionReasonsList(@PathVariable String studentID) { - logger.debug("getAllStudentUndoCompletionReasonsList : "); + log.debug("getAllStudentUndoCompletionReasonsList : "); return response.GET(studentUndoCompletionReasonService.getAllStudentUndoCompletionReasonsList(UUID.fromString(studentID))); } @@ -72,8 +73,8 @@ public ResponseEntity> getAllStudentUndoComple @PreAuthorize(PermissionsContants.CREATE_GRAD_STUDENT_UNGRAD_REASONS_DATA) @Operation(summary = "Create an Ungrad Reasons", description = "Create an Ungrad Reasons", tags = { "Ungrad Reasons" }) @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "400", description = "BAD REQUEST")}) - public ResponseEntity> createGradStudentUndoCompletionReason(@PathVariable String studentID,@Valid @RequestBody StudentUndoCompletionReason gradStudentUndoCompletionReasons) { - logger.debug("createUndoCompletionReason : "); + public ResponseEntity> createGradStudentUndoCompletionReason(@PathVariable String studentID,@Valid @RequestBody StudentUndoCompletionReason gradStudentUndoCompletionReasons) { + log.debug("createUndoCompletionReason : "); validation.requiredField(gradStudentUndoCompletionReasons.getGraduationStudentRecordID(), "Student ID"); validation.requiredField(gradStudentUndoCompletionReasons.getUndoCompletionReasonCode(), "Ungrad Reason Code"); if(validation.hasErrors()) { @@ -90,7 +91,7 @@ public ResponseEntity> createGradS @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "204", description = "NO CONTENT.") }) public ResponseEntity> getAllUndoCompletionReasonCodeList() { - logger.debug("getAllUndoCompletionReasonCodeList : "); + log.debug("getAllUndoCompletionReasonCodeList : "); return response.GET(ungradReasonService.getAllUndoCompletionReasonCodeList()); } @@ -101,7 +102,7 @@ public ResponseEntity> getAllUndoCompletionReasonCode @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "204", description = "NO CONTENT.") }) public ResponseEntity getSpecificUndoCompletionReasonCode(@PathVariable String reasonCode) { - logger.debug("getSpecificUndoCompletionReasonCode : "); + log.debug("getSpecificUndoCompletionReasonCode : "); UndoCompletionReason gradResponse = ungradReasonService.getSpecificUndoCompletionReasonCode(reasonCode); if (gradResponse != null) { return response.GET(gradResponse); @@ -119,7 +120,7 @@ public ResponseEntity getSpecificUndoCompletionReasonCode( @ApiResponse(responseCode = "400", description = "BAD REQUEST") }) public ResponseEntity> createUndoCompletionReason( @Valid @RequestBody UndoCompletionReason gradUndoCompletionReasons) { - logger.debug("createGradUndoCompletionReason : "); + log.debug("createGradUndoCompletionReason : "); validation.requiredField(gradUndoCompletionReasons.getCode(), REASON_CODE); validation.requiredField(gradUndoCompletionReasons.getDescription(), "Reason Description"); if (validation.hasErrors()) { @@ -137,7 +138,7 @@ public ResponseEntity> createUndoCompleti @ApiResponse(responseCode = "400", description = "BAD REQUEST") }) public ResponseEntity> updateUndoCompletionReason( @Valid @RequestBody UndoCompletionReason gradUndoCompletionReasons) { - logger.debug("updateUndoCompletionReason : "); + log.debug("updateUndoCompletionReason : "); validation.requiredField(gradUndoCompletionReasons.getCode(), REASON_CODE); validation.requiredField(gradUndoCompletionReasons.getDescription(), "Reason Description"); if (validation.hasErrors()) { @@ -154,7 +155,7 @@ public ResponseEntity> updateUndoCompleti @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "400", description = "BAD REQUEST") }) public ResponseEntity deleteUndoCompletionReason(@Valid @PathVariable String reasonCode) { - logger.debug("deleteGradUndoCompletionReason : "); + log.debug("deleteGradUndoCompletionReason : "); validation.requiredField(reasonCode, REASON_CODE); if (validation.hasErrors()) { validation.stopOnErrors(); diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/exception/ServiceException.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/exception/ServiceException.java new file mode 100644 index 0000000..eab9207 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/exception/ServiceException.java @@ -0,0 +1,39 @@ +package ca.bc.gov.educ.api.studentgraduation.exception; + +import lombok.Data; + +@Data +public class ServiceException extends RuntimeException { + + private int statusCode; + + public ServiceException() { + super(); + } + + public ServiceException(String message) { + super(message); + } + + public ServiceException(String message, Throwable cause) { + super(message, cause); + } + + public ServiceException(Throwable cause) { + super(cause); + } + + protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public ServiceException(String message, int value) { + super(message); + this.statusCode = value; + } + + public ServiceException(String s, int value, Exception e) { + super(s, e); + this.statusCode = value; + } +} \ No newline at end of file diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleService.java index fa458db..b80ec5c 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleService.java @@ -1,76 +1,67 @@ package ca.bc.gov.educ.api.studentgraduation.service; - import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; - import ca.bc.gov.educ.api.studentgraduation.model.dto.*; import ca.bc.gov.educ.api.studentgraduation.model.transformer.TranscriptMessageTransformer; import ca.bc.gov.educ.api.studentgraduation.repository.TranscriptMessageRepository; import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; -import ca.bc.gov.educ.api.studentgraduation.util.ThreadLocalStateUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.core.ParameterizedTypeReference; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; - import ca.bc.gov.educ.api.studentgraduation.model.transformer.LetterGradeTransformer; import ca.bc.gov.educ.api.studentgraduation.model.transformer.ProgramAlgorithmRuleTransformer; import ca.bc.gov.educ.api.studentgraduation.model.transformer.SpecialCaseTransformer; import ca.bc.gov.educ.api.studentgraduation.repository.LetterGradeRepository; import ca.bc.gov.educ.api.studentgraduation.repository.ProgramAlgorithmRuleRepository; import ca.bc.gov.educ.api.studentgraduation.repository.SpecialCaseRepository; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; - +@Slf4j @Service public class AlgorithmRuleService { - @Autowired - private ProgramAlgorithmRuleTransformer programAlgorithmRuleTransformer; - - @Autowired - private ProgramAlgorithmRuleRepository programAlgorithmRuleRepository; - - @Autowired - private LetterGradeTransformer letterGradeTransformer; - - @Autowired - private LetterGradeRepository letterGradeRepository; - - @Autowired - private SpecialCaseTransformer specialCaseTransformer; - - @Autowired - private SpecialCaseRepository specialCaseRepository; - - @Autowired - private TranscriptMessageTransformer transcriptMessageTransformer; - - @Autowired - private TranscriptMessageRepository transcriptMessageRepository; - - @Autowired - WebClient webClient; + private final ProgramAlgorithmRuleTransformer programAlgorithmRuleTransformer; + private final ProgramAlgorithmRuleRepository programAlgorithmRuleRepository; + private final LetterGradeTransformer letterGradeTransformer; + private final LetterGradeRepository letterGradeRepository; + private final SpecialCaseTransformer specialCaseTransformer; + private final SpecialCaseRepository specialCaseRepository; + private final TranscriptMessageTransformer transcriptMessageTransformer; + private final TranscriptMessageRepository transcriptMessageRepository; + WebClient studentGraduationApiClient; + private final RESTService restService; - @Autowired - RestTemplate restTemplate; - - @Autowired - GradValidation validation; - - @Autowired EducGradStudentGraduationApiConstants constants; - @SuppressWarnings("unused") - private static Logger logger = LoggerFactory.getLogger(AlgorithmRuleService.class); + @Autowired + public AlgorithmRuleService(ProgramAlgorithmRuleTransformer programAlgorithmRuleTransformer, + ProgramAlgorithmRuleRepository programAlgorithmRuleRepository, + LetterGradeTransformer letterGradeTransformer, + LetterGradeRepository letterGradeRepository, + SpecialCaseTransformer specialCaseTransformer, + SpecialCaseRepository specialCaseRepository, + TranscriptMessageTransformer transcriptMessageTransformer, + TranscriptMessageRepository transcriptMessageRepository, + @Qualifier("studentGraduationApiClient") WebClient studentGraduationApiClient, + RESTService restService, EducGradStudentGraduationApiConstants constants) { + this.programAlgorithmRuleTransformer = programAlgorithmRuleTransformer; + this.programAlgorithmRuleRepository = programAlgorithmRuleRepository; + this.letterGradeTransformer = letterGradeTransformer; + this.letterGradeRepository = letterGradeRepository; + this.specialCaseTransformer = specialCaseTransformer; + this.specialCaseRepository = specialCaseRepository; + this.transcriptMessageTransformer = transcriptMessageTransformer; + this.transcriptMessageRepository = transcriptMessageRepository; + this.studentGraduationApiClient = studentGraduationApiClient; + this.restService = restService; + this.constants = constants; + } - public List getAlgorithmRulesList(String programCode) { + public List getAlgorithmRulesList(String programCode) { List responseList = programAlgorithmRuleTransformer.transformToDTO(programAlgorithmRuleRepository.getAlgorithmRulesByProgramCode(programCode)); responseList.sort(Comparator.comparing(ProgramAlgorithmRule::getSortOrder)); return responseList; @@ -103,14 +94,12 @@ public StudentGraduationAlgorithmData getAllAlgorithmData(String programCode) { return data; } - public List getAllAlgorithmDataList(String accessToken) { + public List getAllAlgorithmDataList() { List sList = new ArrayList<>(); - List pList = webClient.get() - .uri(constants.getProgramList()) - .headers(h -> h.setBearerAuth(accessToken)) - .retrieve() - .bodyToMono(new ParameterizedTypeReference>(){}) - .block(); + List pList = restService.get( + String.format(constants.getProgramList()), + new ParameterizedTypeReference>() { + }, studentGraduationApiClient); if(pList != null && !pList.isEmpty()) pList.forEach(p-> sList.add(getAllAlgorithmData(p.getProgramCode()))); return sList; diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeService.java index 57dbb81..27cfbf8 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeService.java @@ -1,52 +1,33 @@ package ca.bc.gov.educ.api.studentgraduation.service; - import java.util.List; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.WebClient; - import ca.bc.gov.educ.api.studentgraduation.model.dto.LetterGrade; import ca.bc.gov.educ.api.studentgraduation.model.entity.LetterGradeEntity; import ca.bc.gov.educ.api.studentgraduation.model.transformer.LetterGradeTransformer; import ca.bc.gov.educ.api.studentgraduation.repository.LetterGradeRepository; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; @Service public class LetterGradeService { - @Autowired - private LetterGradeTransformer letterGradeTransformer; - - @Autowired - private LetterGradeRepository letterGradeRepository; - - @Autowired - GradValidation validation; - - @Autowired - RestTemplate restTemplate; - - @Autowired - WebClient webClient; + private final LetterGradeTransformer letterGradeTransformer; + private final LetterGradeRepository letterGradeRepository; - @SuppressWarnings("unused") - private static Logger logger = LoggerFactory.getLogger(LetterGradeService.class); + @Autowired + public LetterGradeService(LetterGradeTransformer letterGradeTransformer, LetterGradeRepository letterGradeRepository) { + this.letterGradeTransformer = letterGradeTransformer; + this.letterGradeRepository = letterGradeRepository; + } public List getAllLetterGradesList() { return letterGradeTransformer.transformToDTO(letterGradeRepository.findAll()); } public LetterGrade getSpecificLetterGrade(String letterGrade) { - Optional gradResponse =letterGradeRepository.findById(letterGrade); - if(gradResponse.isPresent()) { - return letterGradeTransformer.transformToDTO(gradResponse.get()); - } - return null; - } + Optional gradResponse =letterGradeRepository.findById(letterGrade); + return gradResponse.map(letterGradeTransformer::transformToDTO).orElse(null); + } } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/RESTService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/RESTService.java new file mode 100644 index 0000000..f548114 --- /dev/null +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/RESTService.java @@ -0,0 +1,130 @@ +package ca.bc.gov.educ.api.studentgraduation.service; + +import ca.bc.gov.educ.api.studentgraduation.exception.ServiceException; +import ca.bc.gov.educ.api.studentgraduation.util.EducGradStudentGraduationApiConstants; +import ca.bc.gov.educ.api.studentgraduation.util.ThreadLocalStateUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.io.IOException; +import java.time.Duration; + +@Service +public class RESTService { + + private final WebClient webClient; + + private static final String SERVER_ERROR = "5xx error."; + private static final String SERVICE_FAILED_ERROR = "Service failed to process after max retries."; + + @Autowired + public RESTService(WebClient webClient) { + this.webClient = webClient; + } + + public T get(String url, Class clazz, WebClient webClient) { + T obj; + if (webClient == null) + webClient = this.webClient; + try { + obj = webClient + .get() + .uri(url) + .headers(h -> h.set(EducGradStudentGraduationApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID())) + .retrieve() + // if 5xx errors, throw Service error + .onStatus(HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new ServiceException(getErrorMessage(url, SERVER_ERROR), clientResponse.statusCode().value()))) + .bodyToMono(clazz) + // only does retry if initial error was 5xx as service may be temporarily down + // 4xx errors will always happen if 404, 401, 403 etc, so does not retry + .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)) + .filter(ex -> ex instanceof ServiceException || ex instanceof IOException || ex instanceof WebClientRequestException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + throw new ServiceException(getErrorMessage(url, SERVICE_FAILED_ERROR), HttpStatus.SERVICE_UNAVAILABLE.value()); + })) + .block(); + } catch (Exception e) { + // catches IOExceptions and the like + throw new ServiceException( + getErrorMessage(url, e.getLocalizedMessage()), + (e instanceof WebClientResponseException ex) ? ex.getStatusCode().value() + : HttpStatus.SERVICE_UNAVAILABLE.value(), e); + } + return obj; + } + + public T get(String url, ParameterizedTypeReference typeRef, WebClient webClient) { + T obj; + if (webClient == null) + webClient = this.webClient; + try { + obj = webClient + .get() + .uri(url) + .headers(h -> h.set(EducGradStudentGraduationApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID())) + .retrieve() + // if 5xx errors, throw Service error + .onStatus(HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new ServiceException(getErrorMessage(url, SERVER_ERROR), clientResponse.statusCode().value()))) + .bodyToMono(typeRef) + // only does retry if initial error was 5xx as service may be temporarily down + // 4xx errors will always happen if 404, 401, 403 etc, so does not retry + .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)) + .filter(ex -> ex instanceof ServiceException || ex instanceof IOException || ex instanceof WebClientRequestException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + throw new ServiceException(getErrorMessage(url, SERVICE_FAILED_ERROR), HttpStatus.SERVICE_UNAVAILABLE.value()); + })) + .block(); + } catch (Exception e) { + // catches IOExceptions and the like + throw new ServiceException( + getErrorMessage(url, e.getLocalizedMessage()), + (e instanceof WebClientResponseException ex) ? ex.getStatusCode().value() + : HttpStatus.SERVICE_UNAVAILABLE.value(), e); + } + return obj; + } + + public T post(String url, Object body, Class clazz, WebClient webClient) { + T obj; + if (webClient == null) + webClient = this.webClient; + try { + obj = webClient.post() + .uri(url) + .headers(h -> h.set(EducGradStudentGraduationApiConstants.CORRELATION_ID, ThreadLocalStateUtil.getCorrelationID())) + .body(BodyInserters.fromValue(body)) + .retrieve() + .onStatus(HttpStatusCode::is5xxServerError, + clientResponse -> Mono.error(new ServiceException(getErrorMessage(url, SERVER_ERROR), clientResponse.statusCode().value()))) + .bodyToMono(clazz) + .retryWhen(Retry.backoff(3, Duration.ofSeconds(2)) + .filter(ex -> ex instanceof ServiceException || ex instanceof IOException || ex instanceof WebClientRequestException) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + throw new ServiceException(getErrorMessage(url, SERVICE_FAILED_ERROR), HttpStatus.SERVICE_UNAVAILABLE.value()); + })) + .block(); + } catch (Exception e) { + throw new ServiceException(getErrorMessage( + url, + e.getLocalizedMessage()), + (e instanceof WebClientResponseException ex) ? ex.getStatusCode().value() + : HttpStatus.SERVICE_UNAVAILABLE.value(), e); + } + return obj; + } + + private String getErrorMessage(String url, String errorMessage) { + return "Service failed to process at url: " + url + " due to: " + errorMessage; + } +} diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseService.java index 9625d3d..5eb6449 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseService.java @@ -13,7 +13,6 @@ import ca.bc.gov.educ.api.studentgraduation.model.entity.SpecialCaseCodeEntity; import ca.bc.gov.educ.api.studentgraduation.model.transformer.SpecialCaseTransformer; import ca.bc.gov.educ.api.studentgraduation.repository.SpecialCaseRepository; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; @Service public class SpecialCaseService { @@ -24,10 +23,6 @@ public class SpecialCaseService { @Autowired private SpecialCaseRepository specialCaseRepository; - @Autowired - GradValidation validation; - - @SuppressWarnings("unused") private static Logger logger = LoggerFactory.getLogger(SpecialCaseService.class); diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonService.java index 12d5b7b..bf57653 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonService.java @@ -12,9 +12,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.WebClient; - import ca.bc.gov.educ.api.studentgraduation.model.dto.StudentUndoCompletionReason; import ca.bc.gov.educ.api.studentgraduation.model.dto.UndoCompletionReason; import ca.bc.gov.educ.api.studentgraduation.model.entity.StudentUndoCompletionReasonEntity; @@ -35,12 +32,6 @@ public class StudentUndoCompletionReasonService { @Autowired private UndoCompletionReasonService undoCompletionReasonService; - @Autowired - WebClient webClient; - - @Autowired - RestTemplate restTemplate; - @Autowired GradValidation validation; diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageService.java index efea346..8f2bda9 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageService.java @@ -5,13 +5,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - import ca.bc.gov.educ.api.studentgraduation.model.dto.TranscriptMessage; import ca.bc.gov.educ.api.studentgraduation.model.entity.TranscriptMessageEntity; import ca.bc.gov.educ.api.studentgraduation.model.transformer.TranscriptMessageTransformer; import ca.bc.gov.educ.api.studentgraduation.repository.TranscriptMessageRepository; -import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; @Service public class TranscriptMessageService { @@ -21,12 +18,6 @@ public class TranscriptMessageService { @Autowired private TranscriptMessageTransformer transcriptMessageTransformer; - @Autowired - GradValidation validation; - - @Autowired - WebClient webClient; - public List getAllTranscriptMessageList() { return transcriptMessageTransformer.transformToDTO(transcriptMessageRepository.findAll()); } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonService.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonService.java index 114c647..06e9594 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonService.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonService.java @@ -11,8 +11,6 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.reactive.function.client.WebClient; import jakarta.transaction.Transactional; import jakarta.validation.Valid; @@ -34,12 +32,6 @@ public class UndoCompletionReasonService { @Autowired GradValidation validation; - @Autowired - WebClient webClient; - - @Autowired - RestTemplate restTemplate; - private static final String CREATED_BY="createdBy"; private static final String CREATED_TIMESTAMP="createdTimestamp"; diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/EducGradStudentGraduationApiConstants.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/EducGradStudentGraduationApiConstants.java index 7eba62a..2b2a56c 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/EducGradStudentGraduationApiConstants.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/EducGradStudentGraduationApiConstants.java @@ -65,4 +65,7 @@ public class EducGradStudentGraduationApiConstants { @Value("${endpoint.grad-program-api.get-all-program.url}") private String programList; + + @Value("${endpoint.keycloak.token-uri}") + private String tokenUrl; } diff --git a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/LogHelper.java b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/LogHelper.java index 1b35d5f..4f2b106 100644 --- a/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/LogHelper.java +++ b/api/src/main/java/ca/bc/gov/educ/api/studentgraduation/util/LogHelper.java @@ -22,7 +22,7 @@ public final class LogHelper { private static final ObjectMapper mapper = new ObjectMapper(); private static final String EXCEPTION = "Exception "; - JsonTransformer jsonTransformer; + static JsonTransformer jsonTransformer; @Autowired public LogHelper(JsonTransformer jsonTransformer) { @@ -58,7 +58,7 @@ public void logServerHttpReqResponseDetails(@NonNull final HttpServletRequest re } } - public void logClientHttpReqResponseDetails(@NonNull final HttpMethod method, final String url, final int responseCode, final List correlationID, + public static void logClientHttpReqResponseDetails(@NonNull final HttpMethod method, final String url, final int responseCode, final List correlationID, final List requestSource, final boolean logging) { if (!logging) return; try { diff --git a/api/src/main/resources/application.yaml b/api/src/main/resources/application.yaml index 2724db6..e6bbd11 100644 --- a/api/src/main/resources/application.yaml +++ b/api/src/main/resources/application.yaml @@ -39,6 +39,16 @@ spring: jwt: issuer-uri: ${TOKEN_ISSUER_URL} jwk-set-uri: ${TOKEN_ISSUER_URL}/protocol/openid-connect/certs + client: + registration: + student-graduation-api-client: + client-id: ${GRAD_STUDENT_GRADUATION_API_CLIENT_NAME} + client-secret: ${GRAD_STUDENT_GRADUATION_API_CLIENT_SECRET} + authorization-grant-type: client_credentials + provider: + student-graduation-api-client: + issuer-uri: ${TOKEN_ISSUER_URL} + token-uri: ${TOKEN_ISSUER_URL}/protocol/openid-connect/token #Logging properties logging: @@ -80,6 +90,11 @@ server: io: 16 #port: ${HTTP_PORT} max-http-request-header-size: 20000 + compression: + enabled: ${ENABLE_COMPRESSION} + mime-types: application/json,application/xml,text/html,text/xml,text/plain,text/css,text/javascript,application/javascript + min-response-size: 2048 + excluded-user-agents: MSIE 6.0,UCBrowser #API Documentation springdoc: @@ -95,6 +110,8 @@ splunk: enabled: ${ENABLE_SPLUNK_LOG_HELPER} endpoint: + keycloak: + token-uri: ${TOKEN_ISSUER_URL}/protocol/openid-connect/token grad-program-api: get-all-program: url: ${GRAD_PROGRAM_API}api/v1/program/programs diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleControllerTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleControllerTest.java index 205ba6f..ce3812b 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleControllerTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/controller/AlgorithmRuleControllerTest.java @@ -2,8 +2,6 @@ import ca.bc.gov.educ.api.studentgraduation.model.dto.*; import ca.bc.gov.educ.api.studentgraduation.service.AlgorithmRuleService; -import ca.bc.gov.educ.api.studentgraduation.service.LetterGradeService; -import ca.bc.gov.educ.api.studentgraduation.service.SpecialCaseService; import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; import ca.bc.gov.educ.api.studentgraduation.util.MessageHelper; import ca.bc.gov.educ.api.studentgraduation.util.ResponseHelper; @@ -20,7 +18,7 @@ @ExtendWith(MockitoExtension.class) -public class AlgorithmRuleControllerTest { +class AlgorithmRuleControllerTest { @Mock private AlgorithmRuleService algorithmRuleService; @@ -38,8 +36,7 @@ public class AlgorithmRuleControllerTest { MessageHelper messagesHelper; @Test - public void testGetAlgorithmRulesList() { - List gradList = new ArrayList<>(); + void testGetAlgorithmRulesList() { ProgramAlgorithmRule obj = new ProgramAlgorithmRule(); AlgorithmRuleCode code = new AlgorithmRuleCode(); code.setAlgoRuleCode("A"); @@ -51,8 +48,7 @@ public void testGetAlgorithmRulesList() { } @Test - public void testGetAllAlgorithmRulesList() { - List gradList = new ArrayList<>(); + void testGetAllAlgorithmRulesList() { ProgramAlgorithmRule obj = new ProgramAlgorithmRule(); AlgorithmRuleCode code = new AlgorithmRuleCode(); code.setAlgoRuleCode("A"); @@ -64,7 +60,7 @@ public void testGetAllAlgorithmRulesList() { } @Test - public void testGetAllAlgorithmData() { + void testGetAllAlgorithmData() { StudentGraduationAlgorithmData data = new StudentGraduationAlgorithmData(); data.setLetterGrade(new ArrayList<>()); data.setSpecialCase(new ArrayList<>()); @@ -74,12 +70,12 @@ public void testGetAllAlgorithmData() { } @Test - public void testGetAllAlgorithmDataList() { + void testGetAllAlgorithmDataList() { StudentGraduationAlgorithmData data = new StudentGraduationAlgorithmData(); data.setLetterGrade(new ArrayList<>()); data.setSpecialCase(new ArrayList<>()); - Mockito.when(algorithmRuleService.getAllAlgorithmDataList("accessToken")).thenReturn(List.of(data)); - algorithmRuleController.getAllAlgorithmDataList("accessToken"); - Mockito.verify(algorithmRuleService).getAllAlgorithmDataList("accessToken"); + Mockito.when(algorithmRuleService.getAllAlgorithmDataList()).thenReturn(List.of(data)); + algorithmRuleController.getAllAlgorithmDataList(); + Mockito.verify(algorithmRuleService).getAllAlgorithmDataList(); } } diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleServiceTest.java index 824fb1c..df04261 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/AlgorithmRuleServiceTest.java @@ -22,6 +22,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.core.ParameterizedTypeReference; @@ -61,7 +62,11 @@ public class AlgorithmRuleServiceTest { GradValidation validation; @MockBean - WebClient webClient; + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; + + @Autowired + RESTService restService; @Autowired EducGradStudentGraduationApiConstants constants; @@ -90,16 +95,18 @@ public void init() { @Test public void testGetAllAlgorithmDataList() throws Exception { - String accessToken = "accessToken"; GraduationProgramCode code = new GraduationProgramCode(); code.setProgramCode("2018-EN"); - when(this.webClient.get()).thenReturn(this.requestHeadersUriMock); - when(this.requestHeadersUriMock.uri(constants.getProgramList())).thenReturn(this.requestHeadersMock); + when(this.studentGraduationApiClient.get()).thenReturn(this.requestHeadersUriMock); + when(this.requestHeadersUriMock.uri(any(String.class))).thenReturn(this.requestHeadersMock); when(this.requestHeadersMock.headers(any(Consumer.class))).thenReturn(this.requestHeadersMock); when(this.requestHeadersMock.retrieve()).thenReturn(this.responseMock); - when(this.responseMock.bodyToMono(new ParameterizedTypeReference>(){})).thenReturn(Mono.just(List.of(code))); + when(this.responseMock.onStatus(any(), any())).thenReturn(this.responseMock); + when(this.responseMock.bodyToMono(new ParameterizedTypeReference>(){})) + .thenReturn(Mono.just(List.of(code))); + - List res = algorithmRuleService.getAllAlgorithmDataList(accessToken); + List res = algorithmRuleService.getAllAlgorithmDataList(); assertNotNull(res); assertThat(res).hasSize(1); } diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeServiceTest.java index bd92647..8ca9160 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/LetterGradeServiceTest.java @@ -6,9 +6,9 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mock; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; @@ -34,9 +34,10 @@ public class LetterGradeServiceTest { @Autowired GradValidation validation; - - @Mock - WebClient webClient; + + @MockBean + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; @Test public void testGetAllLetterGradeList() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServiceGETTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServiceGETTest.java new file mode 100644 index 0000000..292caa0 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServiceGETTest.java @@ -0,0 +1,162 @@ +package ca.bc.gov.educ.api.studentgraduation.service; + +import ca.bc.gov.educ.api.studentgraduation.exception.ServiceException; +import ca.bc.gov.educ.api.studentgraduation.model.dto.GraduationProgramCode; +import io.netty.channel.ConnectTimeoutException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import reactor.core.publisher.Mono; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@SpringBootTest +@RunWith(SpringRunner.class) +@ActiveProfiles("test") +public class RESTServiceGETTest { + + @Autowired + private RESTService restService; + + @MockBean + public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; + + @MockBean + public OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + + @MockBean + public ClientRegistrationRepository clientRegistrationRepository; + + @MockBean + @Qualifier("studentGraduationApiClient") + public WebClient studentGraduationApiWebClient; + + @MockBean + private WebClient.RequestHeadersSpec requestHeadersMock; + @MockBean + private WebClient.RequestHeadersUriSpec requestHeadersUriMock; + @MockBean + private WebClient.RequestBodySpec requestBodyMock; + @MockBean + private WebClient.RequestBodyUriSpec requestBodyUriMock; + @MockBean + private WebClient.ResponseSpec responseMock; + + private static final String TEST_URL_200 = "https://httpstat.us/200"; + private static final String TEST_URL_403 = "https://httpstat.us/403"; + private static final String TEST_URL_503 = "https://httpstat.us/503"; + private static final String OK_RESPONSE = "200 OK"; + private static final ParameterizedTypeReference> refType = new ParameterizedTypeReference>() { + }; + + @Before + public void setUp(){ + when(this.studentGraduationApiWebClient.get()).thenReturn(this.requestHeadersUriMock); + when(this.requestHeadersUriMock.uri(any(String.class))).thenReturn(this.requestHeadersMock); + when(this.requestHeadersMock.headers(any(Consumer.class))).thenReturn(this.requestHeadersMock); + when(this.requestHeadersMock.retrieve()).thenReturn(this.responseMock); + when(this.responseMock.onStatus(any(), any())).thenReturn(this.responseMock); + } + + @Test + public void testGet_GivenProperData_Expect200Response(){ + when(this.responseMock.bodyToMono(String.class)).thenReturn(Mono.just(OK_RESPONSE)); + String response = this.restService.get(TEST_URL_200, String.class, studentGraduationApiWebClient); + Assert.assertEquals(OK_RESPONSE, response); + } + + @Test + public void testGet_GivenNullWebClient_Expect200Response(){ + when(this.responseMock.bodyToMono(String.class)).thenReturn(Mono.just(OK_RESPONSE)); + String response = this.restService.get(TEST_URL_200, String.class, null); + Assert.assertEquals(OK_RESPONSE, response); + } + + @Test + public void testGetTypeRef_GivenProperData_Expect200Response(){ + when(this.responseMock.bodyToMono(refType)).thenReturn(Mono.just(new ArrayList())); + List response = this.restService.get(TEST_URL_200, refType, studentGraduationApiWebClient); + Assert.assertEquals(new ArrayList(), response); + } + + @Test + public void testGetTypeRef_GivenNullWebClient_Expect200Response(){ + when(this.responseMock.bodyToMono(refType)).thenReturn(Mono.just(new ArrayList())); + List response = this.restService.get(TEST_URL_200, refType, null); + Assert.assertEquals(new ArrayList(), response); + } + + @Test(expected = ServiceException.class) + public void testGet_Given5xxErrorFromService_ExpectServiceError(){ + when(this.responseMock.bodyToMono(ServiceException.class)).thenReturn(Mono.just(new ServiceException())); + this.restService.get(TEST_URL_503, String.class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGetTypeRef_Given5xxErrorFromService_ExpectServiceError(){ + when(this.responseMock.bodyToMono(refType)).thenThrow(new ServiceException()); + this.restService.get(TEST_URL_503, refType, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGet_Given4xxErrorFromService_ExpectServiceError(){ + when(this.responseMock.bodyToMono(ServiceException.class)).thenReturn(Mono.just(new ServiceException())); + this.restService.get(TEST_URL_403, String.class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGetTypeRef_Given4xxErrorFromService_ExpectServiceError(){ + when(this.responseMock.bodyToMono(ServiceException.class)).thenReturn(Mono.just(new ServiceException())); + this.restService.get(TEST_URL_403, refType, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGet_Given5xxErrorFromService_ExpectConnectionError(){ + when(requestBodyUriMock.uri(TEST_URL_503)).thenReturn(requestBodyMock); + when(requestBodyMock.retrieve()).thenReturn(responseMock); + + when(responseMock.bodyToMono(String.class)).thenReturn(Mono.error(new ConnectTimeoutException("Connection closed"))); + restService.get(TEST_URL_503, String.class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGet_Given5xxErrorFromService_ExpectWebClientRequestError(){ + when(requestBodyUriMock.uri(TEST_URL_503)).thenReturn(requestBodyMock); + when(requestBodyMock.retrieve()).thenReturn(responseMock); + + Throwable cause = new RuntimeException("Simulated cause"); + when(responseMock.bodyToMono(String.class)).thenReturn(Mono.error(new WebClientRequestException(cause, HttpMethod.GET, null, new HttpHeaders()))); + restService.get(TEST_URL_503, String.class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testGetTypeRef_Given5xxErrorFromService_ExpectWebClientRequestError(){ + when(requestBodyUriMock.uri(TEST_URL_503)).thenReturn(requestBodyMock); + when(requestBodyMock.retrieve()).thenReturn(responseMock); + + Throwable cause = new RuntimeException("Simulated cause"); + when(responseMock.bodyToMono(String.class)).thenReturn(Mono.error(new WebClientRequestException(cause, HttpMethod.GET, null, new HttpHeaders()))); + restService.get(TEST_URL_503, refType, studentGraduationApiWebClient); + } + +} diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServicePOSTTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServicePOSTTest.java new file mode 100644 index 0000000..53697b0 --- /dev/null +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/RESTServicePOSTTest.java @@ -0,0 +1,129 @@ +package ca.bc.gov.educ.api.studentgraduation.service; + +import ca.bc.gov.educ.api.studentgraduation.exception.ServiceException; +import ca.bc.gov.educ.api.studentgraduation.util.ThreadLocalStateUtil; +import io.netty.channel.ConnectTimeoutException; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.BodyInserter; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientRequestException; +import reactor.core.publisher.Mono; + +import java.util.function.Consumer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@SpringBootTest +@RunWith(SpringRunner.class) +@ExtendWith(MockitoExtension.class) +@ActiveProfiles("test") +public class RESTServicePOSTTest { + + @Autowired + private RESTService restService; + + @MockBean + private WebClient.RequestHeadersSpec requestHeadersMock; + @MockBean + private WebClient.RequestBodySpec requestBodyMock; + @MockBean + private WebClient.RequestBodyUriSpec requestBodyUriMock; + @MockBean + private WebClient.ResponseSpec responseMock; + @MockBean + public OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; + + @MockBean + public OAuth2AuthorizedClientService oAuth2AuthorizedClientService; + + @MockBean + public ClientRegistrationRepository clientRegistrationRepository; + + @MockBean + @Qualifier("studentGraduationApiClient") + public WebClient studentGraduationApiWebClient; + + + private static final byte[] TEST_BYTES = "The rain in Spain stays mainly on the plain.".getBytes(); + private static final String TEST_BODY = "{test:test}"; + private static final String TEST_URL = "https://fake.url.com"; + + @Before + public void setUp(){ + Mockito.reset(studentGraduationApiWebClient, responseMock, requestHeadersMock, requestBodyMock, requestBodyUriMock); + ThreadLocalStateUtil.clear(); + when(this.studentGraduationApiWebClient.post()).thenReturn(this.requestBodyUriMock); + when(this.requestBodyUriMock.uri(any(String.class))).thenReturn(this.requestBodyMock); + when(this.requestBodyMock.headers(any(Consumer.class))).thenReturn(this.requestBodyMock); + when(this.requestBodyMock.contentType(any())).thenReturn(this.requestBodyMock); + when(this.requestBodyMock.body(any(BodyInserter.class))).thenReturn(this.requestHeadersMock); + when(this.requestHeadersMock.retrieve()).thenReturn(this.responseMock); + when(this.responseMock.bodyToMono(byte[].class)).thenReturn(Mono.just(TEST_BYTES)); + } + + @Test + public void testPost_GivenProperData_Expect200Response(){ + ThreadLocalStateUtil.setCorrelationID("test-correlation-id"); + ThreadLocalStateUtil.setCurrentUser("test-user"); + when(this.responseMock.onStatus(any(), any())).thenReturn(this.responseMock); + byte[] response = this.restService.post(TEST_URL, TEST_BODY, byte[].class, studentGraduationApiWebClient); + Assert.assertArrayEquals(TEST_BYTES, response); + } + + @Test + public void testPostOverride_GivenNullWebClient_Expect200Response(){ + ThreadLocalStateUtil.setCorrelationID("test-correlation-id"); + ThreadLocalStateUtil.setCurrentUser("test-user"); + when(this.responseMock.onStatus(any(), any())).thenReturn(this.responseMock); + byte[] response = this.restService.post(TEST_URL, TEST_BODY, byte[].class, null); + Assert.assertArrayEquals(TEST_BYTES, response); + } + + @Test(expected = ServiceException.class) + public void testPost_Given4xxErrorFromService_ExpectServiceError() { + ThreadLocalStateUtil.setCorrelationID("test-correlation-id"); + ThreadLocalStateUtil.setCurrentUser("test-user"); + when(this.responseMock.onStatus(any(), any())).thenThrow(new ServiceException()); + this.restService.post(TEST_URL, TEST_BODY, byte[].class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testPost_Given5xxErrorFromService_ExpectConnectionError(){ + when(requestBodyUriMock.uri(TEST_URL)).thenReturn(requestBodyMock); + when(requestBodyMock.retrieve()).thenReturn(responseMock); + + when(responseMock.bodyToMono(byte[].class)).thenReturn(Mono.error(new ConnectTimeoutException("Connection closed"))); + this.restService.post(TEST_URL, TEST_BODY, byte[].class, studentGraduationApiWebClient); + } + + @Test(expected = ServiceException.class) + public void testPost_Given5xxErrorFromService_ExpectWebClientRequestError(){ + ThreadLocalStateUtil.setCorrelationID("test-correlation-id"); + ThreadLocalStateUtil.setCurrentUser("test-user"); + when(requestBodyUriMock.uri(TEST_URL)).thenReturn(requestBodyMock); + when(requestBodyMock.retrieve()).thenReturn(responseMock); + + Throwable cause = new RuntimeException("Simulated cause"); + when(responseMock.bodyToMono(byte[].class)).thenReturn(Mono.error(new WebClientRequestException(cause, HttpMethod.POST, null, new HttpHeaders()))); + this.restService.post(TEST_URL, TEST_BODY, byte[].class, studentGraduationApiWebClient); + } + +} diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseServiceTest.java index 68df826..8cbed89 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/SpecialCaseServiceTest.java @@ -8,6 +8,7 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; @@ -17,6 +18,7 @@ import ca.bc.gov.educ.api.studentgraduation.model.entity.SpecialCaseCodeEntity; import ca.bc.gov.educ.api.studentgraduation.repository.SpecialCaseRepository; import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; +import org.springframework.web.reactive.function.client.WebClient; @RunWith(SpringRunner.class) @@ -32,6 +34,10 @@ public class SpecialCaseServiceTest { @Autowired GradValidation validation; + + @MockBean + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; @Test public void testGetAllSpecialCaseList() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonServiceTest.java index e718a05..172c607 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/StudentUndoCompletionReasonServiceTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; @@ -22,6 +23,7 @@ import ca.bc.gov.educ.api.studentgraduation.model.entity.StudentUndoCompletionReasonEntity; import ca.bc.gov.educ.api.studentgraduation.repository.StudentUndoCompletionReasonRepository; import ca.bc.gov.educ.api.studentgraduation.util.GradBusinessRuleException; +import org.springframework.web.reactive.function.client.WebClient; @RunWith(SpringRunner.class) @SpringBootTest @@ -37,6 +39,9 @@ public class StudentUndoCompletionReasonServiceTest { @MockBean private StudentUndoCompletionReasonRepository gradStudentUndoCompletionReasonsRepository; + @MockBean + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; @Test public void testGetAllStudentUndoCompletionReasonsList() { @@ -131,7 +136,7 @@ public void testCreateGradStudentUndoCompletionReasonWithExistingEntity_thenRetu when(this.gradStudentUndoCompletionReasonsRepository.findById(ungradReasonID)).thenReturn(optional); try { - var result = studentUndoCompletionReasonService.createStudentUndoCompletionReason(studentUndoCompletionReason); + studentUndoCompletionReasonService.createStudentUndoCompletionReason(studentUndoCompletionReason); Assertions.fail("Business Exception should have been thrown!"); } catch (GradBusinessRuleException gbre) { assertThat(gbre.getMessage()).isNotNull(); diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageServiceTest.java index 1bdea57..ff509ad 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/TranscriptMessageServiceTest.java @@ -9,10 +9,12 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.client.WebClient; import java.sql.Date; import java.util.ArrayList; @@ -39,7 +41,10 @@ public class TranscriptMessageServiceTest { @Autowired GradValidation validation; - + + @MockBean + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; @Test public void testGetAllMessagingCodeList() { diff --git a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonServiceTest.java b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonServiceTest.java index 59d5760..832aa0c 100644 --- a/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonServiceTest.java +++ b/api/src/test/java/ca/bc/gov/educ/api/studentgraduation/service/UndoCompletionReasonServiceTest.java @@ -12,6 +12,7 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; @@ -23,10 +24,10 @@ import ca.bc.gov.educ.api.studentgraduation.repository.UndoCompletionReasonRepository; import ca.bc.gov.educ.api.studentgraduation.util.GradBusinessRuleException; import ca.bc.gov.educ.api.studentgraduation.util.GradValidation; +import org.springframework.web.reactive.function.client.WebClient; import static org.assertj.core.api.Assertions.assertThat; - @RunWith(SpringRunner.class) @SpringBootTest @ActiveProfiles("test") @@ -46,8 +47,11 @@ public class UndoCompletionReasonServiceTest { @Autowired GradValidation validation; - - + + @MockBean + @Qualifier("studentGraduationApiClient") + WebClient studentGraduationApiClient; + @Test public void testGetAllUndoCompletionReasonCodeList() { List gradUndoCompletionReasonList = new ArrayList<>(); diff --git a/api/src/test/resources/application.yaml b/api/src/test/resources/application.yaml index d6a6c1b..2f1a201 100644 --- a/api/src/test/resources/application.yaml +++ b/api/src/test/resources/application.yaml @@ -73,6 +73,8 @@ splunk: enabled: false endpoint: + keycloak: + token-uri: http://my-keycloak.com grad-program-api: get-all-program: url: https://educ-grad-program-api-77c02f-dev.apps.silver.devops.gov.bc.ca/api/v1/programs \ No newline at end of file diff --git a/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru b/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read Algorithm Data by ProgramCode.bru similarity index 77% rename from tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru rename to tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read Algorithm Data by ProgramCode.bru index 4b19436..fb1c918 100644 --- a/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru +++ b/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read Algorithm Data by ProgramCode.bru @@ -1,5 +1,5 @@ meta { - name: Read All Data required by Grad Algorithm + name: Read Algorithm Data by ProgramCode type: http seq: 2 } diff --git a/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru b/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru new file mode 100644 index 0000000..a70091a --- /dev/null +++ b/tools/bruno/EDUC-GRAD-STUDENT-GRADUATION-API/Algorithm/Read All Data required by Grad Algorithm.bru @@ -0,0 +1,11 @@ +meta { + name: Read All Data required by Grad Algorithm + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/api/v1/studentgraduation/algo/algorithmdata + body: none + auth: none +} diff --git a/tools/config/clients-and-scopes.js b/tools/config/clients-and-scopes.js new file mode 100644 index 0000000..8ae967b --- /dev/null +++ b/tools/config/clients-and-scopes.js @@ -0,0 +1,178 @@ +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +// Load config +const configPath = path.resolve(__dirname, 'clients-config.json'); +const clients = JSON.parse(fs.readFileSync(configPath, 'utf8')); +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); // for self-signed certs + +const keycloakUrl = process.env.KEYCLOAK_URL; +const realm = process.env.KEYCLOAK_REALM; +const openshiftApi = process.env.OPENSHIFT_SERVER; +const gradNamespace = `${process.env.GRAD_NAMESPACE}-${process.env.TARGET_ENV}`; +const openshiftNamespace = process.env.OPENSHIFT_NAMESPACE; +const openshiftToken = process.env.OPENSHIFT_TOKEN; + +async function getOpenShiftSecret(openshiftApi, openshiftToken, namespace, secretName) { + const url = `${openshiftApi}/api/v1/namespaces/${namespace}/secrets/${secretName}`; + + try { + const resp = await axios.get(url, { + headers: { Authorization: `Bearer ${openshiftToken}` }, + httpsAgent + }); + + const encodedData = resp.data.data; + const decodedData = {}; + + for (const [key, value] of Object.entries(encodedData)) { + decodedData[key] = Buffer.from(value, 'base64').toString('utf-8'); + } + + return decodedData; + } catch (err) { + throw new Error(`Failed to retrieve secret "${secretName}": ${err.response?.data?.message || err.message}`); + } +} + +async function getAccessToken({username, password}) { + const url = `${keycloakUrl}/auth/realms/${realm}/protocol/openid-connect/token`; + const params = new URLSearchParams(); + params.append('grant_type', 'password'); + params.append('client_id', 'admin-cli'); + params.append('username', username); + params.append('password', password); + + const response = await axios.post(url, params); + return response.data.access_token; +} + +async function getClientByClientId(token, clientId) { + const url = `${keycloakUrl}/auth/admin/realms/${realm}/clients?clientId=${encodeURIComponent(clientId)}`; + const headers = { Authorization: `Bearer ${token}` }; + const response = await axios.get(url, { headers }); + + return response.data.length > 0 ? response.data[0] : null; +} + +async function createClient(token, client, secret) { + const url = `${keycloakUrl}/auth/admin/realms/${realm}/clients`; + const headers = { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }; + + const data = { + clientId: client.clientId, + secret: secret, + ...client.settings + }; + + const response = await axios.post(url, data, { headers }); + return response.headers.location.split('/').pop(); +} + +async function getClientUUID(accessToken, targetClientId) { + const url = `${keycloakUrl}/auth/admin/realms/${realm}/clients`; + const res = await axios.get(url, { + headers: { Authorization: `Bearer ${accessToken}` }, + params: { clientId: targetClientId } + }); + + const client = res.data.find(c => c.clientId === targetClientId); + if (!client) throw new Error(`Client '${targetClientId}' not found.`); + return client.id; +} + +async function deleteClient(token, targetClientId) { + try { + const clientUUID = await getClientUUID(token, targetClientId); + + const deleteUrl = `${keycloakUrl}/auth/admin/realms/${realm}/clients/${clientUUID}`; + await axios.delete(deleteUrl, { + headers: { Authorization: `Bearer ${token}` } + }); + + console.log(`✅ Client '${targetClientId}' deleted.`); + } catch (err) { + console.error(`❌ Error deleting client:`, err.response?.data || err.message); + } +} + +async function ensureScopeExists(token, scopeName) { + const headers = { Authorization: `Bearer ${token}` }; + const scopesUrl = `${keycloakUrl}/auth/admin/realms/${realm}/client-scopes`; + + const response = await axios.get(scopesUrl, { headers }); + let scope = response.data.find(s => s.name === scopeName.trim()); + + if (!scope) { + console.log(`➕ Creating missing scope "${scopeName}"...`); + await axios.post(scopesUrl, { + name: scopeName, + protocol: "openid-connect", + attributes: { + "include.in.token.scope": "true", + "display.on.consent.screen": "true" + } + }, { headers }); + + const updatedList = await axios.get(scopesUrl, { headers }); + scope = updatedList.data.find(s => s.name === scopeName.trim()); + } + + return scope; +} + +async function assignScopes(token, clientId, scopeNames) { + const headers = { Authorization: `Bearer ${token}` }; + + for (const scopeName of scopeNames || []) { + const scope = await ensureScopeExists(token, scopeName); + if (!scope) continue; + + const assignUrl = `${keycloakUrl}/auth/admin/realms/${realm}/clients/${clientId}/default-client-scopes/${scope.id}`; + try { + await axios.put(assignUrl, null, { headers }); + console.log(`✅ Assigned scope "${scope.name}" to client.`); + } catch (err) { + if (err.response?.status === 409) { + console.log(`â„šī¸ Scope "${scope.name}" already assigned.`); + } else { + console.error(`❌ Failed to assign scope "${scope.name}":`, err.message); + } + } + } +} + +(async () => { + try { + const kcCredentials = await getOpenShiftSecret(openshiftApi, openshiftToken, gradNamespace, 'grad-kc-admin'); + const token = await getAccessToken(kcCredentials); + + for (const client of clients) { + console.log(`🚀 Processing client "${client.clientId}"...`); + let existingClient = await getClientByClientId(token, client.clientId); + let clientIdValue; + let clientSecret; + + if (existingClient) { + clientSecret = existingClient.secret; + await deleteClient(token, client.clientId); + console.log(`🔄 Deleted client "${client.clientId}".`); + } + + clientIdValue = await createClient(token, client, clientSecret); + console.log(`➕ Created client "${client.clientId}".`); + + await assignScopes(token, clientIdValue, client.scopes); + } + + console.log(`✅ All clients processed.`); + } catch (err) { + console.error('❌ Error:', err.response?.data || err.message); + process.exit(1); + } +})(); \ No newline at end of file diff --git a/tools/config/clients-config.json b/tools/config/clients-config.json new file mode 100644 index 0000000..7448f21 --- /dev/null +++ b/tools/config/clients-config.json @@ -0,0 +1,24 @@ +[ + { + "clientId": "grad-student-graduation-api-client", + "scopes": ["CREATE_GRAD_STUDENT_UNGRAD_REASONS_DATA", + "CREATE_GRAD_UNGRAD_CODE_DATA", + "DELETE_GRAD_UNGRAD_CODE_DATA", + "READ_GRAD_ALGORITHM_RULES_DATA", + "READ_GRAD_LETTER_GRADE_DATA", + "READ_GRAD_MESSAGING_CODE_DATA", + "READ_GRAD_SPECIAL_CASE_DATA", + "READ_GRAD_STUDENT_UNGRAD_REASONS_DATA", + "READ_GRAD_UNGRAD_CODE_DATA", + "UPDATE_GRAD_UNGRAD_CODE_DATA", + "READ_GRAD_PROGRAM_CODE_DATA"], + "settings": { + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "directAccessGrantsEnabled": false + } + } +] \ No newline at end of file diff --git a/tools/openshift/api.dc.yaml b/tools/openshift/api.dc.yaml index 58c5bfd..9d6ff97 100644 --- a/tools/openshift/api.dc.yaml +++ b/tools/openshift/api.dc.yaml @@ -62,6 +62,8 @@ objects: name: educ-grad-student-graduation-api-config-map - secretRef: name: api-student-graduation-db-secret + - secretRef: + name: grad-student-graduation-api-client-secret resources: requests: cpu: "${MIN_CPU}" diff --git a/tools/openshift/clients.json b/tools/openshift/clients.json new file mode 100644 index 0000000..18e3049 --- /dev/null +++ b/tools/openshift/clients.json @@ -0,0 +1,3 @@ +{ + "clients": ["grad-student-graduation-api-client"] +} \ No newline at end of file diff --git a/tools/openshift/fetch-and-create-secrets.js b/tools/openshift/fetch-and-create-secrets.js new file mode 100644 index 0000000..295600b --- /dev/null +++ b/tools/openshift/fetch-and-create-secrets.js @@ -0,0 +1,118 @@ +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); +const https = require('https'); + +const keycloakUrl = process.env.KEYCLOAK_URL; +const realm = process.env.KEYCLOAK_REALM; +const openshiftApi = process.env.OPENSHIFT_SERVER; +const gradNamespace = `${process.env.GRAD_NAMESPACE}-${process.env.TARGET_ENV}`; +const openshiftNamespace = process.env.OPENSHIFT_NAMESPACE; +const openshiftToken = process.env.OPENSHIFT_TOKEN; + +const config = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'clients.json'), 'utf8')); +const httpsAgent = new https.Agent({ rejectUnauthorized: false }); // for self-signed certs + +async function getOpenShiftSecret(openshiftApi, openshiftToken, namespace, secretName) { + const url = `${openshiftApi}/api/v1/namespaces/${namespace}/secrets/${secretName}`; + + try { + const resp = await axios.get(url, { + headers: { Authorization: `Bearer ${openshiftToken}` }, + httpsAgent + }); + + const encodedData = resp.data.data; + const decodedData = {}; + + for (const [key, value] of Object.entries(encodedData)) { + decodedData[key] = Buffer.from(value, 'base64').toString('utf-8'); + } + + return decodedData; + } catch (err) { + throw new Error(`Failed to retrieve secret "${secretName}": ${err.response?.data?.message || err.message}`); + } +} + +async function getAccessToken({username, password}) { + const url = `${keycloakUrl}/auth/realms/${realm}/protocol/openid-connect/token`; + const params = new URLSearchParams(); + params.append('grant_type', 'password'); + params.append('client_id', 'admin-cli'); + params.append('username', username); + params.append('password', password); + + const response = await axios.post(url, params); + return response.data.access_token; +} + +async function getClientCredentials(token, clientId) { + const headers = { Authorization: `Bearer ${token}` }; + const searchUrl = `${keycloakUrl}/auth/admin/realms/${realm}/clients?clientId=${encodeURIComponent(clientId)}`; + const clientResp = await axios.get(searchUrl, { headers }); + + if (!clientResp.data.length) throw new Error(`Client "${clientId}" not found`); + + const client = clientResp.data[0]; + const secretUrl = `${keycloakUrl}/auth/admin/realms/${realm}/clients/${client.id}/client-secret`; + const secretResp = await axios.get(secretUrl, { headers }); + + return { + clientId: client.clientId, + secret: secretResp.data.value + }; +} + +async function createOpenshiftSecret({ clientId, secret }) { + const url = `${openshiftApi}/api/v1/namespaces/${openshiftNamespace}/secrets`; + const headers = { + Authorization: `Bearer ${openshiftToken}`, + 'Content-Type': 'application/json' + }; + + const secretName = `${clientId}-secret`; + + const payload = { + apiVersion: 'v1', + kind: 'Secret', + metadata: { + name: secretName + }, + type: 'Opaque', + data: { + [`${clientId}-NAME`.toUpperCase().replaceAll('-', '_')]: Buffer.from(clientId).toString('base64'), + [`${clientId}-SECRET`.toUpperCase().replaceAll('-', '_')]: Buffer.from(secret).toString('base64') + } + }; + + try { + await axios.post(url, payload, { headers }); + console.log(`✅ Secret "${secretName}" created.`); + } catch (err) { + if (err.response?.status === 409) { + console.log(`🔁 Secret "${secretName}" already exists. Replacing...`); + await axios.put(`${url}/${secretName}`, payload, { headers }); + } else { + console.error(`❌ Failed to create secret for "${clientId}":`, err.message); + } + } +} + +(async () => { + try { + const kcCredentials = await getOpenShiftSecret(openshiftApi, openshiftToken, gradNamespace, 'grad-kc-admin'); + const kcToken = await getAccessToken(kcCredentials); + + for (const clientId of config.clients) { + console.log(`🔍 Fetching secret for "${clientId}"...`); + const creds = await getClientCredentials(kcToken, clientId); + await createOpenshiftSecret(creds); + } + + console.log('🎉 All secrets processed.'); + } catch (err) { + console.error('❌ Error:', err.response?.data || err.message); + process.exit(1); + } +})(); \ No newline at end of file